@moldable-ai/ui 0.2.8 → 0.2.9

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 (38) hide show
  1. package/dist/components/app-error-boundary.d.ts +26 -0
  2. package/dist/components/app-error-boundary.d.ts.map +1 -0
  3. package/dist/components/app-error-boundary.js +87 -0
  4. package/dist/components/chat/chat-input.d.ts +8 -1
  5. package/dist/components/chat/chat-input.d.ts.map +1 -1
  6. package/dist/components/chat/chat-input.js +6 -3
  7. package/dist/components/chat/chat-message.d.ts +1 -0
  8. package/dist/components/chat/chat-message.d.ts.map +1 -1
  9. package/dist/components/chat/chat-message.js +100 -12
  10. package/dist/components/chat/chat-messages.d.ts +2 -1
  11. package/dist/components/chat/chat-messages.d.ts.map +1 -1
  12. package/dist/components/chat/chat-messages.js +3 -3
  13. package/dist/components/chat/chat-panel.d.ts +25 -3
  14. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  15. package/dist/components/chat/chat-panel.js +158 -42
  16. package/dist/components/chat/index.d.ts +3 -3
  17. package/dist/components/chat/index.d.ts.map +1 -1
  18. package/dist/components/chat/index.js +1 -1
  19. package/dist/components/chat/model-selector.d.ts.map +1 -1
  20. package/dist/components/chat/model-selector.js +1 -1
  21. package/dist/components/chat/tool-handlers.d.ts.map +1 -1
  22. package/dist/components/chat/tool-handlers.js +89 -1
  23. package/dist/components/markdown.d.ts.map +1 -1
  24. package/dist/components/markdown.js +16 -2
  25. package/dist/components/rich-media-player.d.ts +8 -0
  26. package/dist/components/rich-media-player.d.ts.map +1 -0
  27. package/dist/components/rich-media-player.js +99 -0
  28. package/dist/components/ui/resizable.d.ts +4 -1
  29. package/dist/components/ui/resizable.d.ts.map +1 -1
  30. package/dist/components/ui/resizable.js +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -0
  34. package/package.json +4 -1
  35. package/src/styles/index.css +25 -0
  36. package/dist/lib/commands.test.d.ts +0 -2
  37. package/dist/lib/commands.test.d.ts.map +0 -1
  38. package/dist/lib/commands.test.js +0 -111
@@ -1,36 +1,102 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { AlertCircle, ChevronDown, Code2, CornerDownRight, Image as ImageIcon, ListPlus, MessageCircle, X, } from 'lucide-react';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { AlertCircle, ArrowDown, ChevronDown, ClipboardList, Code2, CornerDownRight, GripVertical, Image as ImageIcon, ListPlus, MessageCircle, Pencil, Trash2, } from 'lucide-react';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
5
5
  import { cn } from '../../lib/utils';
6
6
  import { Button } from '../ui/button';
7
7
  import { ScrollArea } from '../ui/scroll-area';
8
8
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../ui/tooltip';
9
9
  import { ChatInput, } from './chat-input';
10
- import { ThinkingMessage } from './chat-message';
10
+ import { CompactingMessage, ThinkingMessage, } from './chat-message';
11
11
  import { MessageRow, computeIsThinking, } from './chat-messages';
12
12
  import { ConversationHistory, } from './conversation-history';
13
13
  import { ModelSelector } from './model-selector';
14
14
  import { ReasoningEffortSelector, } from './reasoning-effort-selector';
15
15
  import { ToolApprovalProvider, } from './tool-approval-context';
16
16
  import { ToolProgressProvider } from './tool-progress-context';
17
+ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from '@dnd-kit/core';
18
+ import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
19
+ import { CSS } from '@dnd-kit/utilities';
17
20
  import { AnimatePresence, motion } from 'framer-motion';
18
21
  import { Virtualizer } from 'virtua';
22
+ function clampPercent(value) {
23
+ if (!Number.isFinite(value))
24
+ return 0;
25
+ return Math.max(0, Math.min(100, value));
26
+ }
27
+ function formatTokenCount(tokens) {
28
+ if (tokens < 1_000)
29
+ return `${tokens}`;
30
+ if (tokens < 1_000_000)
31
+ return `${Math.round(tokens / 1_000)}k`;
32
+ const millions = tokens / 1_000_000;
33
+ return `${Number.isInteger(millions) ? millions.toFixed(0) : millions.toFixed(2)}M`;
34
+ }
35
+ function ContextWindowIndicator({ usage }) {
36
+ if (!usage || usage.maxTokens <= 0)
37
+ return null;
38
+ const usedPercent = clampPercent(Math.round((usage.usedTokens / usage.maxTokens) * 100));
39
+ const ringColor = usedPercent >= 90
40
+ ? 'var(--destructive)'
41
+ : usedPercent >= 75
42
+ ? 'var(--warning)'
43
+ : 'var(--muted-foreground)';
44
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground focus-visible:ring-ring flex size-8 cursor-pointer items-center justify-center rounded-full bg-transparent p-0 focus-visible:outline-none focus-visible:ring-2", "aria-label": `Context window ${usedPercent}% full`, children: _jsx("span", { className: "relative size-4 rounded-full", style: {
45
+ background: `conic-gradient(${ringColor} ${usedPercent}%, color-mix(in oklch, var(--muted-foreground) 22%, transparent) 0)`,
46
+ }, children: _jsx("span", { className: "bg-background absolute inset-[3px] rounded-full" }) }) }) }), _jsxs(TooltipContent, { align: "end", side: "top", sideOffset: 8, className: "w-56 px-3 py-2.5 text-center shadow-lg", children: [_jsx("div", { className: "opacity-70", children: "Context window:" }), _jsxs("div", { className: "mt-1 text-sm font-medium", children: [usedPercent, "% full"] }), _jsxs("div", { className: "mt-1 text-xs", children: [formatTokenCount(usage.usedTokens), " /", ' ', formatTokenCount(usage.maxTokens), " tokens used"] }), usage.compactsAutomatically && (_jsx("div", { className: "mt-2.5 text-xs font-medium leading-4", children: "Moldable automatically compacts its context" }))] })] }));
47
+ }
48
+ function ContextCompactedDivider() {
49
+ return (_jsx("div", { "data-testid": "context-compacted-divider", className: "text-muted-foreground/80 w-full px-4 py-2", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("div", { className: "bg-border h-px min-w-8 flex-1" }), _jsxs("div", { className: "flex select-none items-center gap-2 whitespace-nowrap text-[13px] font-medium leading-5", children: [_jsx(ClipboardList, { className: "size-4" }), _jsx("span", { children: "Context automatically compacted" })] }), _jsx("div", { className: "bg-border h-px min-w-8 flex-1" })] }) }));
50
+ }
51
+ function QueuedMessageRow({ message, isResponding, onRetryQueuedMessage, onSteerQueuedMessage, onRemoveQueuedMessage, onEditQueuedMessage, }) {
52
+ const isSteering = message.status === 'steering';
53
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: message.id, disabled: isSteering });
54
+ const style = {
55
+ transform: CSS.Transform.toString(transform),
56
+ transition,
57
+ };
58
+ return (_jsxs("div", { ref: setNodeRef, style: style, className: cn('bg-muted/60 text-foreground flex min-h-9 items-center gap-2 rounded-lg px-2 py-1.5 text-xs', isDragging && 'relative z-20 opacity-80 shadow-lg'), children: [_jsx("button", { type: "button", className: "text-muted-foreground hover:text-foreground flex size-6 shrink-0 cursor-grab items-center justify-center rounded-md transition-colors active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-40", "aria-label": "Reorder queued follow-up", disabled: isSteering, ...attributes, ...listeners, children: _jsx(GripVertical, { className: "size-3.5" }) }), _jsxs("span", { className: "min-w-0 flex-1 truncate", children: [message.preview, message.attachmentCount > 0 &&
59
+ ` (${message.attachmentCount} image${message.attachmentCount === 1 ? '' : 's'})`] }), message.error && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", onClick: (event) => event.stopPropagation(), className: "text-destructive hover:bg-destructive/10 flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "View queued follow-up error", children: _jsx(AlertCircle, { className: "size-3.5" }) }) }), _jsx(TooltipContent, { side: "top", className: "max-w-80", children: _jsx("p", { className: "break-words text-xs", children: message.error }) })] }) })), message.status === 'failed' && onRetryQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
60
+ event.stopPropagation();
61
+ onRetryQueuedMessage(message.id);
62
+ }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Retry queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Retry" })] })), isSteering && (_jsx("span", { className: "text-muted-foreground shrink-0 px-2 text-xs", children: "Steering" })), !isSteering && isResponding && onSteerQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
63
+ event.stopPropagation();
64
+ onSteerQueuedMessage(message.id);
65
+ }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Steer active response with queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Steer" })] })), !isSteering && onEditQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
66
+ event.stopPropagation();
67
+ onEditQueuedMessage(message.id);
68
+ }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Edit queued follow-up", children: _jsx(Pencil, { className: "size-3.5" }) })), !isSteering && onRemoveQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
69
+ event.stopPropagation();
70
+ onRemoveQueuedMessage(message.id);
71
+ }, className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Remove queued follow-up", children: _jsx(Trash2, { className: "size-3.5" }) }))] }));
72
+ }
19
73
  /**
20
74
  * Floating chat panel with model selector
21
75
  */
22
- export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, isMinimized = false, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress = {}, onApprovalResponse, isEditingApp = true, onEditingAppChange, showEditingAppToggle = false, checkpoints, restoringMessageId, onRestoreCheckpoint, imageAttachments = [], onRemoveImageAttachment, isImageDropActive = false, queuedMessages = [], onRemoveQueuedMessage, onRetryQueuedMessage, onSteerQueuedMessage, }) {
76
+ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, scrollToBottomSignal = 0, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, isMinimized = false, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress = {}, onApprovalResponse, isEditingApp = true, onEditingAppChange, showEditingAppToggle = false, checkpoints, restoringMessageId, onRestoreCheckpoint, imageAttachments = [], onRemoveImageAttachment, isImageDropActive = false, queuedMessages = [], onRemoveQueuedMessage, onRetryQueuedMessage, onSteerQueuedMessage, onReorderQueuedMessages, onEditQueuedMessage, slashCommands, onSlashCommandSelect, isCompacting = false, compactedHistoryStartIndex, contextUsage, }) {
23
77
  const inputRef = useRef(null);
24
78
  const scrollAreaRef = useRef(null);
25
79
  const viewportRef = useRef(null);
26
80
  const virtualizerRef = useRef(null);
27
81
  const prevMessageCountRef = useRef(messages.length);
82
+ const prevConversationIdRef = useRef(currentConversationId ?? null);
83
+ const prevScrollToBottomSignalRef = useRef(scrollToBottomSignal);
84
+ const pendingConversationScrollIdRef = useRef(null);
85
+ const hasPendingScrollToBottomSignalRef = useRef(false);
28
86
  const scrollRafRef = useRef(null);
29
87
  const [viewportVersion, setViewportVersion] = useState(0);
30
88
  const [isAtBottom, setIsAtBottom] = useState(true);
89
+ const queuedMessageIds = useMemo(() => queuedMessages.map((message) => message.id), [queuedMessages]);
90
+ const queueSensors = useSensors(useSensor(PointerSensor, {
91
+ activationConstraint: {
92
+ distance: 6,
93
+ },
94
+ }), useSensor(KeyboardSensor, {
95
+ coordinateGetter: sortableKeyboardCoordinates,
96
+ }));
31
97
  const isResponding = status === 'streaming' || status === 'submitted';
32
98
  const controlsDisabled = isTurnInFlight ?? isResponding;
33
- const composerControls = (_jsxs(_Fragment, { children: [_jsx(ModelSelector, { models: models, selectedModel: selectedModel, onModelChange: onModelChange, disabled: controlsDisabled, dropdownAlign: "end", dropdownSide: "top" }), reasoningEffortOptions &&
99
+ const composerControls = (_jsxs(_Fragment, { children: [_jsx(ContextWindowIndicator, { usage: contextUsage }), _jsx(ModelSelector, { models: models, selectedModel: selectedModel, onModelChange: onModelChange, disabled: controlsDisabled, dropdownAlign: "end", dropdownSide: "top" }), reasoningEffortOptions &&
34
100
  selectedReasoningEffort &&
35
101
  onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: controlsDisabled, showLabel: true, dropdownAlign: "end", dropdownSide: "top" }))] }));
36
102
  // Capture the Radix viewport element so virtua can use it as the scroll container
@@ -88,10 +154,6 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
88
154
  if (!input.trim() && imageAttachments.length === 0)
89
155
  return;
90
156
  const submitMode = mode ?? (isResponding ? 'steer' : 'send');
91
- // Steer interrupts the current response and sends the new instruction now.
92
- if (submitMode === 'steer' && isResponding && onStop) {
93
- onStop();
94
- }
95
157
  if (!isExpanded) {
96
158
  onExpandedChange(true);
97
159
  }
@@ -105,9 +167,21 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
105
167
  isExpanded,
106
168
  onExpandedChange,
107
169
  onSubmit,
108
- onStop,
109
170
  submitDisabled,
110
171
  ]);
172
+ const handleQueuedDragEnd = useCallback((event) => {
173
+ const { active, over } = event;
174
+ if (!over || active.id === over.id)
175
+ return;
176
+ onReorderQueuedMessages?.(String(active.id), String(over.id));
177
+ }, [onReorderQueuedMessages]);
178
+ const handleEditQueuedMessage = useCallback((id) => {
179
+ onEditQueuedMessage?.(id);
180
+ if (!isExpanded) {
181
+ onExpandedChange(true);
182
+ }
183
+ window.setTimeout(() => inputRef.current?.focus(), 0);
184
+ }, [isExpanded, onEditQueuedMessage, onExpandedChange]);
111
185
  const handleNewChat = useCallback(() => {
112
186
  onNewChat?.();
113
187
  inputRef.current?.focus();
@@ -156,12 +230,24 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
156
230
  if (messages.length === 0 && welcomeMessage) {
157
231
  items.push({ key: 'welcome', kind: 'welcome' });
158
232
  }
159
- for (const message of messages) {
233
+ const shouldShowCompactedDivider = typeof compactedHistoryStartIndex === 'number' &&
234
+ compactedHistoryStartIndex >= 0 &&
235
+ compactedHistoryStartIndex < messages.length;
236
+ for (const [index, message] of messages.entries()) {
237
+ if (shouldShowCompactedDivider && index === compactedHistoryStartIndex) {
238
+ items.push({
239
+ key: `context-compacted-${compactedHistoryStartIndex}`,
240
+ kind: 'context-compacted-divider',
241
+ });
242
+ }
160
243
  items.push({ key: message.id, kind: 'message', message });
161
244
  }
162
245
  if (isThinking) {
163
246
  items.push({ key: 'thinking', kind: 'thinking' });
164
247
  }
248
+ if (isCompacting) {
249
+ items.push({ key: 'compacting', kind: 'compacting' });
250
+ }
165
251
  if (showMissingApiKeyPrompt) {
166
252
  items.push({ key: 'missing-api-key', kind: 'missing-api-key' });
167
253
  }
@@ -171,6 +257,8 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
171
257
  return items;
172
258
  }, [
173
259
  isThinking,
260
+ isCompacting,
261
+ compactedHistoryStartIndex,
174
262
  messages,
175
263
  showErrorMessage,
176
264
  showMissingApiKeyPrompt,
@@ -180,12 +268,16 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
180
268
  switch (item.kind) {
181
269
  case 'welcome':
182
270
  return (_jsx("div", { className: "px-2 pb-4", children: messages.length === 0 && welcomeMessage && (_jsx("div", { className: "bg-muted/50 text-muted-foreground mx-2 rounded-2xl p-4 text-sm", children: welcomeMessage })) }));
271
+ case 'context-compacted-divider':
272
+ return (_jsx("div", { className: "px-2 pb-3", children: _jsx(ContextCompactedDivider, {}) }));
183
273
  case 'message': {
184
274
  const checkpoint = checkpoints?.get(item.message.id);
185
275
  return (_jsx("div", { className: "px-2 pb-3", children: _jsx(MessageRow, { message: item.message, isLast: item.message.id === lastMessageId, isStreaming: isStreaming, checkpoint: checkpoint, restoringMessageId: restoringMessageId, onRestoreCheckpoint: onRestoreCheckpoint }) }));
186
276
  }
187
277
  case 'thinking':
188
278
  return (_jsx("div", { className: "px-2 pb-3", children: _jsx(ThinkingMessage, {}) }));
279
+ case 'compacting':
280
+ return (_jsx("div", { className: "px-2 pb-3", children: _jsx(CompactingMessage, {}) }));
189
281
  case 'missing-api-key':
190
282
  return (_jsx("div", { className: "px-2 pb-4", children: _jsxs("div", { className: "border-primary/30 bg-primary/5 mx-2 flex flex-col items-center gap-3 rounded-lg border p-4 text-center", children: [_jsx("div", { className: "bg-primary/10 flex size-10 items-center justify-center rounded-full", children: _jsx(AlertCircle, { className: "text-primary size-5" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-foreground font-medium", children: "API key required" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: "Add an API key to Vault to start chatting with AI" })] }), onAddApiKey && (_jsx(Button, { onClick: onAddApiKey, size: "sm", className: "cursor-pointer", children: "Add API key" }))] }) }));
191
283
  case 'error':
@@ -202,35 +294,71 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
202
294
  restoringMessageId,
203
295
  welcomeMessage,
204
296
  ]);
205
- // Scroll to bottom on new messages if the user is already at bottom
206
- useEffect(() => {
207
- const previousMessageCount = prevMessageCountRef.current;
208
- const hasNewMessage = messages.length > previousMessageCount;
209
- prevMessageCountRef.current = messages.length;
210
- if (!isExpanded || !isAtBottom) {
211
- return;
212
- }
297
+ const scrollToBottom = useCallback((smooth = true) => {
213
298
  const lastIndex = listItems.length - 1;
214
299
  if (lastIndex < 0)
215
300
  return;
216
301
  if (scrollRafRef.current !== null) {
217
302
  cancelAnimationFrame(scrollRafRef.current);
218
303
  }
304
+ setIsAtBottom(true);
219
305
  scrollRafRef.current = requestAnimationFrame(() => {
220
306
  virtualizerRef.current?.scrollToIndex(lastIndex, {
221
307
  align: 'end',
222
- smooth: hasNewMessage,
308
+ smooth,
223
309
  });
224
310
  scrollRafRef.current = null;
225
311
  });
312
+ }, [listItems.length]);
313
+ // New conversations loaded from history should start at the latest message.
314
+ useEffect(() => {
315
+ const previousConversationId = prevConversationIdRef.current;
316
+ const nextConversationId = currentConversationId ?? null;
317
+ prevConversationIdRef.current = nextConversationId;
318
+ if (previousConversationId !== nextConversationId) {
319
+ pendingConversationScrollIdRef.current = nextConversationId;
320
+ setIsAtBottom(true);
321
+ }
322
+ if (!nextConversationId ||
323
+ pendingConversationScrollIdRef.current !== nextConversationId ||
324
+ !isExpanded ||
325
+ listItems.length === 0) {
326
+ return;
327
+ }
328
+ scrollToBottom(false);
329
+ pendingConversationScrollIdRef.current = null;
330
+ }, [currentConversationId, isExpanded, listItems.length, scrollToBottom]);
331
+ useEffect(() => {
332
+ if (prevScrollToBottomSignalRef.current !== scrollToBottomSignal) {
333
+ prevScrollToBottomSignalRef.current = scrollToBottomSignal;
334
+ hasPendingScrollToBottomSignalRef.current = true;
335
+ setIsAtBottom(true);
336
+ }
337
+ if (!hasPendingScrollToBottomSignalRef.current ||
338
+ !isExpanded ||
339
+ listItems.length === 0) {
340
+ return;
341
+ }
342
+ scrollToBottom(false);
343
+ hasPendingScrollToBottomSignalRef.current = false;
344
+ }, [isExpanded, listItems.length, scrollToBottom, scrollToBottomSignal]);
345
+ // Scroll to bottom on new messages if the user is already at bottom
346
+ useEffect(() => {
347
+ const previousMessageCount = prevMessageCountRef.current;
348
+ const hasNewMessage = messages.length > previousMessageCount;
349
+ prevMessageCountRef.current = messages.length;
350
+ if (!isExpanded || !isAtBottom) {
351
+ return;
352
+ }
353
+ scrollToBottom(hasNewMessage);
226
354
  return () => {
227
355
  if (scrollRafRef.current !== null) {
228
356
  cancelAnimationFrame(scrollRafRef.current);
229
357
  scrollRafRef.current = null;
230
358
  }
231
359
  };
232
- }, [isAtBottom, isExpanded, listItems.length, messages.length]);
233
- return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && !isMinimized && (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, className: "fixed inset-0 z-40 bg-black/20", onClick: () => onExpandedChange(false) })) }), _jsx(AnimatePresence, { children: isMinimized && (_jsx(motion.div, { initial: { y: 20, opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: 20, opacity: 0 }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "fixed bottom-0 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 -translate-x-1/2", children: _jsxs("button", { onClick: handleFlapClick, className: cn('bg-background/95 hover:bg-background group flex cursor-pointer items-center gap-2 rounded-t-xl border border-b-0 px-4 py-2 shadow-lg backdrop-blur transition-colors', 'text-muted-foreground hover:text-foreground'), children: [_jsx(MessageCircle, { className: "size-4" }), _jsx("span", { className: "text-sm font-medium", children: "Chat" }), _jsx("span", { className: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded px-1.5 py-0.5 text-xs transition-colors", children: "\u2318M" })] }) })) }), _jsx(motion.div, { initial: false, animate: {
360
+ }, [isAtBottom, isExpanded, messages.length, scrollToBottom]);
361
+ return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && !isMinimized && (_jsx(motion.div, { "data-moldable-chat-backdrop": true, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, className: "fixed inset-0 z-40 bg-black/20", onClick: () => onExpandedChange(false) })) }), _jsx(AnimatePresence, { children: isMinimized && (_jsx(motion.div, { "data-moldable-chat-panel": true, initial: { y: 20, opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: 20, opacity: 0 }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "fixed bottom-0 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 -translate-x-1/2", children: _jsxs("button", { onClick: handleFlapClick, className: cn('bg-background/95 hover:bg-background group flex cursor-pointer items-center gap-2 rounded-t-xl border border-b-0 px-4 py-2 shadow-lg backdrop-blur transition-colors', 'text-muted-foreground hover:text-foreground'), children: [_jsx(MessageCircle, { className: "size-4" }), _jsx("span", { className: "text-sm font-medium", children: "Chat" }), _jsx("span", { className: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded px-1.5 py-0.5 text-xs transition-colors", children: "\u2318M" })] }) })) }), _jsx(motion.div, { "data-moldable-chat-panel": true, initial: false, animate: {
234
362
  width: isExpanded ? 'min(90vw, 640px)' : '400px',
235
363
  y: isMinimized ? 'calc(100% + 24px)' : 0,
236
364
  opacity: isMinimized ? 0 : 1,
@@ -256,24 +384,12 @@ export function ChatPanel({ messages, status, isTurnInFlight, input, onInputChan
256
384
  repeat: Infinity,
257
385
  repeatDelay: 5,
258
386
  ease: [0.4, 0, 0.2, 1],
259
- } })), _jsxs(motion.div, { initial: false, animate: {
260
- height: isExpanded ? 'min(60vh, 480px)' : '0px',
261
- opacity: isExpanded ? 1 : 0,
262
- }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "relative z-10 flex flex-col overflow-hidden", style: { pointerEvents: isExpanded ? 'auto' : 'none' }, children: [_jsx("div", { className: "absolute right-3 top-3 z-20", children: _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground cursor-pointer", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] }) }) }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-h-0 min-w-0 flex-1 px-2", children: _jsx("div", { className: "min-w-0 max-w-full overflow-hidden py-4 pr-10", children: _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: viewportVersion > 0 ? (_jsx(Virtualizer, { ref: virtualizerRef, data: listItems, scrollRef: viewportRef, bufferSize: 300, children: (item) => renderVirtualItem(item) }, viewportVersion)) : (listItems.map((item) => (_jsx("div", { children: renderVirtualItem(item) }, item.key)))) }) }) }) })] }), _jsxs("div", { className: "relative z-10", onClick: () => {
263
- if (!isExpanded) {
264
- onExpandedChange(true);
265
- }
266
- }, children: [queuedMessages.length > 0 && (_jsxs("div", { className: "border-border/70 bg-background/95 border-t px-4 py-2", children: [_jsxs("div", { className: "text-muted-foreground mb-2 flex items-center gap-2 text-xs font-medium", children: [_jsx(ListPlus, { className: "size-3.5" }), _jsx("span", { children: "Queued follow-ups" })] }), _jsx("div", { className: "space-y-1.5", children: queuedMessages.map((message) => (_jsxs("div", { className: "bg-muted/60 text-foreground flex min-h-9 items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs", children: [_jsxs("span", { className: "min-w-0 flex-1 truncate", children: [message.preview, message.attachmentCount > 0 &&
267
- ` (${message.attachmentCount} image${message.attachmentCount === 1 ? '' : 's'})`] }), message.status === 'failed' &&
268
- message.error &&
269
- onRetryQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
270
- event.stopPropagation();
271
- onRetryQueuedMessage(message.id);
272
- }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Retry queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Retry" })] })), isResponding && onSteerQueuedMessage && (_jsxs("button", { type: "button", onClick: (event) => {
273
- event.stopPropagation();
274
- onSteerQueuedMessage(message.id);
275
- }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 text-xs transition-colors", "aria-label": "Steer active response with queued follow-up", children: [_jsx(CornerDownRight, { className: "size-3.5" }), _jsx("span", { children: "Steer" })] })), onRemoveQueuedMessage && (_jsx("button", { type: "button", onClick: (event) => {
276
- event.stopPropagation();
277
- onRemoveQueuedMessage(message.id);
278
- }, className: "text-muted-foreground hover:bg-accent hover:text-foreground flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors", "aria-label": "Remove queued follow-up", children: _jsx(X, { className: "size-3.5" }) }))] }, message.id))) })] })), _jsx(ChatInput, { input: input, onInputChange: onInputChange, onSubmit: handleSubmit, isResponding: isResponding, placeholder: placeholder, inputRef: inputRef, onStop: onStop, compact: !isExpanded, imageAttachments: imageAttachments, onRemoveImageAttachment: onRemoveImageAttachment, footerStartControls: isExpanded ? composerStartControls : null, footerControls: isExpanded ? composerControls : null, submitDisabled: submitDisabled })] })] }) })] }));
387
+ } })), _jsxs("div", { className: "relative z-10 overflow-hidden rounded-[inherit]", children: [_jsxs(motion.div, { initial: false, animate: {
388
+ height: isExpanded ? 'min(60vh, 480px)' : '0px',
389
+ opacity: isExpanded ? 1 : 0,
390
+ }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "flex flex-col overflow-hidden", style: { pointerEvents: isExpanded ? 'auto' : 'none' }, children: [_jsx("div", { className: "absolute right-3 top-3 z-20", children: _jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground cursor-pointer", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] }) }) }), _jsxs("div", { className: "relative min-h-0 min-w-0 flex-1", children: [_jsx(ScrollArea, { ref: scrollAreaRef, className: "h-full px-2", children: _jsx("div", { className: "min-w-0 max-w-full overflow-hidden py-4 pr-10", children: _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: viewportVersion > 0 ? (_jsx(Virtualizer, { ref: virtualizerRef, data: listItems, scrollRef: viewportRef, bufferSize: 300, children: (item) => renderVirtualItem(item) }, viewportVersion)) : (listItems.map((item) => (_jsx("div", { children: renderVirtualItem(item) }, item.key)))) }) }) }) }), !isAtBottom && listItems.length > 0 && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { type: "button", variant: "secondary", size: "icon-sm", onClick: () => scrollToBottom(true), className: "absolute bottom-3 right-4 z-20 size-8 cursor-pointer rounded-full shadow-lg", children: _jsx(ArrowDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "top", children: "Scroll to bottom" })] }) }))] })] }), _jsxs("div", { className: "relative z-10", onClick: () => {
391
+ if (!isExpanded) {
392
+ onExpandedChange(true);
393
+ }
394
+ }, children: [queuedMessages.length > 0 && (_jsxs("div", { className: "border-border/70 bg-background/95 border-t px-4 py-2", children: [_jsxs("div", { className: "text-muted-foreground mb-2 flex items-center gap-2 text-xs font-medium", children: [_jsx(ListPlus, { className: "size-3.5" }), _jsx("span", { children: "Queued follow-ups" })] }), _jsx(DndContext, { sensors: queueSensors, collisionDetection: closestCenter, onDragEnd: handleQueuedDragEnd, children: _jsx(SortableContext, { items: queuedMessageIds, strategy: verticalListSortingStrategy, children: _jsx("div", { className: "space-y-1.5", children: queuedMessages.map((message) => (_jsx(QueuedMessageRow, { message: message, isResponding: isResponding, onRetryQueuedMessage: onRetryQueuedMessage, onSteerQueuedMessage: onSteerQueuedMessage, onRemoveQueuedMessage: onRemoveQueuedMessage, onEditQueuedMessage: handleEditQueuedMessage }, message.id))) }) }) })] })), _jsx(ChatInput, { input: input, onInputChange: onInputChange, onSubmit: handleSubmit, isResponding: isResponding, placeholder: placeholder, inputRef: inputRef, onStop: onStop, compact: !isExpanded, imageAttachments: imageAttachments, onRemoveImageAttachment: onRemoveImageAttachment, footerStartControls: isExpanded ? composerStartControls : null, footerControls: isExpanded ? composerControls : null, submitDisabled: submitDisabled, slashCommands: slashCommands, onSlashCommandSelect: onSlashCommandSelect })] })] })] }) })] }));
279
395
  }
@@ -1,12 +1,12 @@
1
- export { ChatInput, type ChatImageAttachment, type ChatSubmitMode, } from './chat-input';
2
- export { Message, ThinkingMessage, type ChatMessage, type ChatMessagePart, } from './chat-message';
1
+ export { ChatInput, type ChatImageAttachment, type ChatSlashCommandOption, type ChatSubmitMode, } from './chat-input';
2
+ export { CompactingMessage, Message, ThinkingMessage, type ChatMessage, type ChatMessagePart, } from './chat-message';
3
3
  export { Messages, type MessageCheckpoint } from './chat-messages';
4
4
  export { CheckpointBadge, type CheckpointBadgeProps } from './checkpoint-badge';
5
5
  export { RestoreDialog, type CheckpointInfo, type RestoreDialogProps, } from './restore-dialog';
6
6
  export { ModelSelector, type ModelOption } from './model-selector';
7
7
  export { ReasoningEffortSelector, type ReasoningEffortOption, } from './reasoning-effort-selector';
8
8
  export { ConversationHistory, type ConversationMeta, } from './conversation-history';
9
- export { ChatPanel, type ChatPanelProps, type QueuedChatMessage, type ToolProgressData, } from './chat-panel';
9
+ export { ChatPanel, type ChatContextUsage, type ChatPanelProps, type QueuedChatMessage, type ToolProgressData, } from './chat-panel';
10
10
  export { ThinkingTimeline, ThinkingTimelineMarker, type ThinkingTimelineItem, type ThinkingTimelineProps, } from './thinking-timeline';
11
11
  export { getToolHandler, DEFAULT_TOOL_HANDLERS, type ToolHandler, } from './tool-handlers';
12
12
  export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,mBAAmB,EACxB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,iBAAiB,EACjB,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAClE,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAA;AAC/E,OAAO,EACL,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAA"}
@@ -1,5 +1,5 @@
1
1
  export { ChatInput, } from './chat-input';
2
- export { Message, ThinkingMessage, } from './chat-message';
2
+ export { CompactingMessage, Message, ThinkingMessage, } from './chat-message';
3
3
  export { Messages } from './chat-messages';
4
4
  export { CheckpointBadge } from './checkpoint-badge';
5
5
  export { RestoreDialog, } from './restore-dialog';
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/model-selector.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC1C,YAAY,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;CACnD,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,aAAa,EACb,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,aAAuB,EACvB,YAAuB,GACxB,EAAE,kBAAkB,2CAyCpB"}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../src/components/chat/model-selector.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAUtC,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;CAChB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAA;IAC1C,YAAY,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAA;CACnD,CAAA;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,aAAa,EACb,aAAa,EACb,QAAgB,EAChB,SAAS,EACT,aAAuB,EACvB,YAAuB,GACxB,EAAE,kBAAkB,2CAwCpB"}
@@ -5,5 +5,5 @@ import { Button } from '../ui/button';
5
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../ui/dropdown-menu';
6
6
  export function ModelSelector({ models, selectedModel, onModelChange, disabled = false, className, dropdownAlign = 'start', dropdownSide = 'bottom', }) {
7
7
  const currentModel = models.find((m) => m.id === selectedModel) ?? models[0];
8
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground h-8 min-w-0 max-w-[180px] cursor-pointer gap-1.5 rounded-full px-2.5 text-xs font-medium', className), children: [_jsx("span", { children: currentModel?.icon }), _jsx("span", { className: "min-w-0 truncate", children: currentModel?.name }), _jsx(ChevronDown, { className: "size-3 opacity-50" })] }) }), _jsx(DropdownMenuContent, { align: dropdownAlign, side: dropdownSide, className: "min-w-[160px]", children: models.map((model) => (_jsxs(DropdownMenuItem, { onClick: () => onModelChange(model.id), className: "flex cursor-pointer items-center gap-2", children: [_jsx("span", { children: model.icon }), _jsx("span", { className: "flex-1", children: model.name }), model.id === selectedModel && (_jsx(Check, { className: "text-primary size-4" }))] }, model.id))) })] }));
8
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", size: "sm", disabled: disabled, className: cn('text-muted-foreground hover:text-foreground h-8 min-w-0 max-w-[180px] cursor-pointer gap-1 rounded-full px-2.5 text-xs font-medium', className), children: [_jsx("span", { className: "min-w-0 truncate", children: currentModel?.name }), _jsx(ChevronDown, { className: "size-3 opacity-50" })] }) }), _jsx(DropdownMenuContent, { align: dropdownAlign, side: dropdownSide, className: "min-w-[160px]", children: models.map((model) => (_jsxs(DropdownMenuItem, { onClick: () => onModelChange(model.id), className: "flex cursor-pointer items-center gap-2", children: [_jsx("span", { children: model.icon }), _jsx("span", { className: "flex-1", children: model.name }), model.id === selectedModel && (_jsx(Check, { className: "text-primary size-4" }))] }, model.id))) })] }));
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAuBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAW5D,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AAEtE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IAExB,YAAY,EAAE,MAAM,CAAA;IAEpB,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,SAAS,CAAA;IAGhE,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,SAAS,CAAA;IAG7C,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,qBAAqB,KAC5B,SAAS,CAAA;IAGd,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,uBAAuB,KAC/B,SAAS,CAAA;CACf,CAAA;AAoSD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAg/D7D,CAAA;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAmC5D"}
1
+ {"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAwBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAW5D,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AAEtE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IAExB,YAAY,EAAE,MAAM,CAAA;IAEpB,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,SAAS,CAAA;IAGhE,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,SAAS,CAAA;IAG7C,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,qBAAqB,KAC5B,SAAS,CAAA;IAGd,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,uBAAuB,KAC/B,SAAS,CAAA;CACf,CAAA;AAoSD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAqvE7D,CAAA;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAmC5D"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { AlertTriangle, BookOpen, Check, CheckCheck, Copy, Database, Download, FileCode, FileText, FolderOpen, Globe, Info, Package, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
3
+ import { AlertTriangle, BookOpen, Camera, Check, CheckCheck, Copy, Database, Download, FileCode, FileText, FolderOpen, Globe, Info, Package, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
4
4
  import { useState } from 'react';
5
5
  import { cn } from '../../lib/utils';
6
6
  import { ThinkingTimelineMarker } from './thinking-timeline';
@@ -108,6 +108,94 @@ function appApiItemCount(result) {
108
108
  * Default tool handlers for Moldable tools
109
109
  */
110
110
  export const DEFAULT_TOOL_HANDLERS = {
111
+ viewImage: {
112
+ loadingLabel: 'Viewing image...',
113
+ marker: ThinkingTimelineMarker.Default,
114
+ inline: false,
115
+ renderLoading: (args) => {
116
+ const { imagePath } = (args ?? {});
117
+ return (_jsx(LoadingIndicator, { icon: Sparkles, children: imagePath
118
+ ? `Viewing ${getFileName(imagePath)}...`
119
+ : 'Viewing image...' }));
120
+ },
121
+ renderOutput: (output, toolCallId) => {
122
+ const result = output;
123
+ if (!result) {
124
+ return (_jsx(LoadingIndicator, { icon: Sparkles, children: "Viewing image..." }, toolCallId));
125
+ }
126
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Sparkles, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: result.imagePath
127
+ ? `Viewed ${getFileName(result.imagePath)}`
128
+ : 'Viewed image' })] }, toolCallId));
129
+ },
130
+ },
131
+ generateImage: {
132
+ loadingLabel: 'Generating image...',
133
+ marker: ThinkingTimelineMarker.Default,
134
+ inline: true,
135
+ renderLoading: (args) => {
136
+ const { prompt, aspectRatio } = (args ?? {});
137
+ const ratioClass = aspectRatio === 'portrait'
138
+ ? 'aspect-[3/4]'
139
+ : aspectRatio === 'story'
140
+ ? 'aspect-[9/16]'
141
+ : aspectRatio === 'landscape'
142
+ ? 'aspect-[4/3]'
143
+ : aspectRatio === 'widescreen'
144
+ ? 'aspect-video'
145
+ : 'aspect-square';
146
+ return (_jsx("figure", { className: "my-2 min-w-0 max-w-2xl", children: _jsxs("div", { className: cn('border-border bg-muted relative w-full overflow-hidden rounded-lg border', ratioClass), children: [_jsx("div", { className: "from-muted via-background/45 to-muted absolute inset-0 animate-pulse bg-gradient-to-br" }), _jsx("div", { className: "bg-foreground/5 absolute inset-0" }), _jsxs("div", { className: "text-muted-foreground absolute inset-0 flex flex-col items-center justify-center gap-2 px-6 text-center", children: [_jsx(Sparkles, { className: "size-5 animate-pulse" }), _jsx("span", { className: "max-w-full truncate text-xs font-medium", children: prompt
147
+ ? `Generating "${prompt.slice(0, 54)}${prompt.length > 54 ? '...' : ''}"`
148
+ : 'Generating image...' })] })] }) }));
149
+ },
150
+ renderOutput: (output, toolCallId) => {
151
+ const result = output;
152
+ if (!result) {
153
+ return (_jsx(LoadingIndicator, { icon: Sparkles, children: "Generating image..." }, toolCallId));
154
+ }
155
+ const parts = Array.isArray(result.value) ? result.value : [];
156
+ const image = parts.find((part) => (part.type === 'image-data' || part.type === 'media') &&
157
+ part.data &&
158
+ part.mediaType?.startsWith('image/'));
159
+ const imageSrc = result.imageUrl ??
160
+ (image?.data && image.mediaType
161
+ ? `data:${image.mediaType};base64,${image.data}`
162
+ : undefined);
163
+ if (!imageSrc) {
164
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 flex max-w-full items-start gap-2 rounded-md px-2 py-1.5 text-xs", children: [_jsx(Sparkles, { className: "mt-0.5 size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 whitespace-normal break-words leading-relaxed", children: result.imagePath
165
+ ? `Generated image saved at ${result.imagePath}.`
166
+ : 'Generated image saved.' })] }, toolCallId));
167
+ }
168
+ return (_jsxs("figure", { className: "my-2 min-w-0 max-w-2xl", children: [_jsx("div", { className: "border-border bg-muted overflow-hidden rounded-lg border", children: _jsx("img", { src: imageSrc, alt: result.prompt || 'Generated image', className: "block max-h-[420px] w-full object-contain" }) }), _jsxs("figcaption", { className: "text-muted-foreground mt-1 flex min-w-0 items-center gap-1 text-xs", children: [_jsx(Sparkles, { className: "size-3 shrink-0" }), _jsx("span", { className: "truncate", children: result.imagePath
169
+ ? `${getFileName(result.imagePath)}${result.size ? ` - ${result.size}` : ''}`
170
+ : result.size || 'Generated image' })] })] }, toolCallId));
171
+ },
172
+ },
173
+ take_screenshot: {
174
+ loadingLabel: 'Taking screenshot...',
175
+ marker: ThinkingTimelineMarker.Default,
176
+ inline: true,
177
+ renderLoading: () => (_jsx(LoadingIndicator, { icon: Camera, children: "Taking screenshot..." })),
178
+ renderOutput: (output, toolCallId) => {
179
+ const result = output;
180
+ if (!result) {
181
+ return (_jsx(LoadingIndicator, { icon: Camera, children: "Taking screenshot..." }, toolCallId));
182
+ }
183
+ if (result.type === 'error-text') {
184
+ return (_jsxs("div", { className: "bg-destructive/10 text-destructive my-1 flex max-w-full items-start gap-2 rounded-md px-2 py-1.5 text-xs", children: [_jsx(Camera, { className: "mt-0.5 size-3.5 shrink-0" }), _jsx("span", { className: "min-w-0 whitespace-normal break-words leading-relaxed", children: typeof result.value === 'string'
185
+ ? result.value
186
+ : 'Screenshot failed' })] }, toolCallId));
187
+ }
188
+ const parts = Array.isArray(result.value) ? result.value : [];
189
+ const image = parts.find((part) => (part.type === 'image-data' || part.type === 'media') &&
190
+ part.data &&
191
+ part.mediaType?.startsWith('image/'));
192
+ const text = parts.find((part) => part.type === 'text')?.text;
193
+ if (!image?.data || !image.mediaType) {
194
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Camera, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: text || 'Screenshot captured' }), _jsx(Check, { className: "size-3 shrink-0 text-green-600" })] }, toolCallId));
195
+ }
196
+ return (_jsxs("figure", { className: "my-2 min-w-0 max-w-2xl", children: [_jsx("div", { className: "border-border bg-muted overflow-hidden rounded-lg border", children: _jsx("img", { src: `data:${image.mediaType};base64,${image.data}`, alt: "Current app screenshot", className: "block max-h-[420px] w-full object-contain" }) }), text && (_jsxs("figcaption", { className: "text-muted-foreground mt-1 flex items-center gap-1 text-xs", children: [_jsx(Camera, { className: "size-3" }), _jsx("span", { className: "truncate", children: text })] }))] }, toolCallId));
197
+ },
198
+ },
111
199
  listMoldableAppApi: {
112
200
  loadingLabel: 'Listing app APIs...',
113
201
  marker: ThinkingTimelineMarker.Default,
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAyB,MAAM,OAAO,CAAA;AAgF7C,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAoSD,eAAO,MAAM,QAAQ,uFA5DlB,mBAAmB,6CAmErB,CAAA"}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAyB,MAAM,OAAO,CAAA;AAiF7C,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA8TD,eAAO,MAAM,QAAQ,uFA5DlB,mBAAmB,6CAmErB,CAAA"}
@@ -4,6 +4,7 @@ import React, { memo, useState } from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import { cn } from '../lib/utils';
6
6
  import { CodeBlock } from './code-block';
7
+ import { RichMediaPlayer } from './rich-media-player';
7
8
  import { Button } from './ui/button';
8
9
  import { Checkbox } from './ui/checkbox';
9
10
  import rehypeRaw from 'rehype-raw';
@@ -62,10 +63,23 @@ function processMentions(text) {
62
63
  return parts.length > 0 ? parts : [text];
63
64
  }
64
65
  const components = {
65
- img({ src, alt }) {
66
+ img({ src, alt, className, style, width }) {
66
67
  if (!src || typeof src !== 'string')
67
68
  return null;
68
- return (_jsx("span", { className: "flex justify-center", children: _jsx("img", { src: src, alt: alt ?? '', className: "h-auto max-w-full rounded-lg" }) }));
69
+ const imageWidth = typeof width === 'number' || typeof width === 'string'
70
+ ? width
71
+ : undefined;
72
+ return (_jsx("span", { className: "flex justify-center", children: _jsx("img", { src: src, alt: alt ?? '', className: cn('h-auto max-w-full rounded-lg', className), style: style, width: imageWidth }) }));
73
+ },
74
+ audio({ src, title }) {
75
+ if (!src || typeof src !== 'string')
76
+ return null;
77
+ return (_jsx(RichMediaPlayer, { kind: "audio", src: src, title: typeof title === 'string' ? title : undefined }));
78
+ },
79
+ video({ src, title }) {
80
+ if (!src || typeof src !== 'string')
81
+ return null;
82
+ return (_jsx(RichMediaPlayer, { kind: "video", src: src, title: typeof title === 'string' ? title : undefined }));
69
83
  },
70
84
  table({ children }) {
71
85
  return (_jsx("table", { className: "border-collapse border px-3 py-1", children: children }));
@@ -0,0 +1,8 @@
1
+ export type RichMediaPlayerProps = {
2
+ className?: string;
3
+ kind: 'audio' | 'video';
4
+ src: string;
5
+ title?: string;
6
+ };
7
+ export declare function RichMediaPlayer({ className, kind, src, title, }: RichMediaPlayerProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=rich-media-player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-media-player.d.ts","sourceRoot":"","sources":["../../src/components/rich-media-player.tsx"],"names":[],"mappings":"AAaA,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAuBD,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,IAAI,EACJ,GAAG,EACH,KAAK,GACN,EAAE,oBAAoB,2CAmNtB"}