@moldable-ai/ui 0.2.2 → 0.2.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.
@@ -11,6 +11,10 @@ export type ChatMessagePart = {
11
11
  state: string;
12
12
  args?: unknown;
13
13
  output?: unknown;
14
+ approval?: {
15
+ id: string;
16
+ approved?: boolean;
17
+ };
14
18
  };
15
19
  export type ChatMessage = {
16
20
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-message.tsx"],"names":[],"mappings":"AAsBA,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAA;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAED,KAAK,YAAY,GAAG;IAClB,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAcD,iBAAS,WAAW,CAAC,EACnB,OAAO,EACP,MAAc,EACd,WAAmB,GACpB,EAAE,YAAY,2CA2Xd;AAED,eAAO,MAAM,OAAO,yDAAoB,CAAA;AAExC,wBAAgB,eAAe,4CAiB9B"}
1
+ {"version":3,"file":"chat-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-message.tsx"],"names":[],"mappings":"AAgCA,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAA;QACV,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;CACF,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAA;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAED,KAAK,YAAY,GAAG;IAClB,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAcD,iBAAS,WAAW,CAAC,EACnB,OAAO,EACP,MAAc,EACd,WAAmB,GACpB,EAAE,YAAY,2CA8bd;AAED,eAAO,MAAM,OAAO,yDAAoB,CAAA;AAExC,wBAAgB,eAAe,4CAiB9B"}
@@ -6,10 +6,19 @@ import { cn } from '../../lib/utils';
6
6
  import { Markdown } from '../markdown';
7
7
  import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../ui/collapsible';
8
8
  import { ThinkingTimeline, ThinkingTimelineMarker, } from './thinking-timeline';
9
+ import { ToolApproval, ToolApprovalHeader, ToolApprovalRejected, } from './tool-approval';
10
+ import { useToolApprovalResponse } from './tool-approval-context';
9
11
  import { getToolHandler } from './tool-handlers';
12
+ import { useToolProgress } from './tool-progress-context';
10
13
  import { AnimatePresence, motion } from 'framer-motion';
11
14
  const DEFAULT_ACTIONS_LABEL = 'Thinking';
15
+ // OpenRouter sends these for encrypted reasoning msgs
16
+ const stripRedacted = (text) => text.replace(/\[REDACTED\]/g, '').trim();
12
17
  function PureMessage({ message, isLast = false, isStreaming = false, }) {
18
+ // Get tool progress for streaming stdout/stderr
19
+ const toolProgress = useToolProgress();
20
+ // Get approval response handler
21
+ const onApprovalResponse = useToolApprovalResponse();
13
22
  // Track open state for each thinking group independently
14
23
  const [thinkingGroupStates, setThinkingGroupStates] = useState(new Map());
15
24
  // Process parts chronologically, grouping only reasoning into thinking groups
@@ -69,13 +78,51 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
69
78
  if (toolHandler.inline) {
70
79
  // Close any open thinking group first
71
80
  closeThinkingGroup();
81
+ // Check for approval states
82
+ const isApprovalRequested = part.state === 'approval-requested';
83
+ const isApprovalResponded = part.state === 'approval-responded' ||
84
+ part.state === 'output-denied';
85
+ const wasRejected = part.approval?.approved === false;
86
+ const wasApproved = part.approval?.approved === true;
72
87
  // Render inline tool
88
+ // Also treat approval-responded (when approved) as loading until output arrives
73
89
  const isToolLoading = part.state === 'partial-call' ||
74
90
  part.state === 'call' ||
75
- part.state === 'pending';
76
- const toolContent = isToolLoading
77
- ? toolHandler.renderLoading?.(part.args) || (_jsx("div", { className: "text-muted-foreground text-xs", children: toolHandler.loadingLabel }))
78
- : toolHandler.renderOutput(part.output, part.toolCallId || key);
91
+ part.state === 'pending' ||
92
+ (isApprovalResponded && wasApproved && part.output === undefined);
93
+ // Check for streaming progress (stdout/stderr from command execution)
94
+ const progress = part.toolCallId
95
+ ? toolProgress[part.toolCallId]
96
+ : undefined;
97
+ const hasStreamingOutput = progress && (progress.stdout || progress.stderr);
98
+ let toolContent;
99
+ // Handle approval request state
100
+ if (isApprovalRequested &&
101
+ toolHandler.renderApproval &&
102
+ part.approval?.id &&
103
+ onApprovalResponse) {
104
+ toolContent = toolHandler.renderApproval({
105
+ approvalId: part.approval.id,
106
+ toolCallId: part.toolCallId || key,
107
+ args: part.args,
108
+ }, onApprovalResponse);
109
+ }
110
+ else if (isApprovalResponded && wasRejected) {
111
+ // Show rejection message
112
+ toolContent = (_jsx(ToolApproval, { state: "output-denied", approved: false, children: _jsx(ToolApprovalHeader, { children: _jsx(ToolApprovalRejected, { children: "Command execution was rejected" }) }) }));
113
+ }
114
+ else if (hasStreamingOutput && toolHandler.renderStreaming) {
115
+ // Show streaming output (live stdout/stderr)
116
+ toolContent = toolHandler.renderStreaming(part.args, progress);
117
+ }
118
+ else if (isToolLoading) {
119
+ // Show loading state (arguments streaming or waiting)
120
+ toolContent = toolHandler.renderLoading?.(part.args) || (_jsx("div", { className: "text-muted-foreground text-xs", children: toolHandler.loadingLabel }));
121
+ }
122
+ else {
123
+ // Show final output
124
+ toolContent = toolHandler.renderOutput(part.output, part.toolCallId || key);
125
+ }
79
126
  items.push({
80
127
  type: 'inline-tool',
81
128
  id: key,
@@ -110,15 +157,15 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
110
157
  }
111
158
  if (part.type === 'reasoning') {
112
159
  const full = part.text ?? '';
113
- const trimmedFull = full.trim();
114
- if (trimmedFull.length === 0) {
160
+ const cleaned = stripRedacted(full);
161
+ if (cleaned.length === 0) {
115
162
  continue;
116
163
  }
117
164
  const group = ensureThinkingGroup();
118
165
  // Extract title from markdown **title** or use first line
119
- const markdownTitleMatch = full.match(/\*\*([^*]+)\*\*/);
166
+ const markdownTitleMatch = cleaned.match(/\*\*([^*]+)\*\*/);
120
167
  const candidateTitle = markdownTitleMatch?.[1];
121
- const trimmed = trimmedFull;
168
+ const trimmed = cleaned;
122
169
  const firstLine = trimmed.split(/\n+/)[0];
123
170
  const extractedTitle = candidateTitle || firstLine?.slice(0, 50) || DEFAULT_ACTIONS_LABEL;
124
171
  group.title = extractedTitle;
@@ -148,7 +195,13 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
148
195
  contentItems: items,
149
196
  hasFinalAssistantText: state.hasText,
150
197
  };
151
- }, [message.id, message.parts, message.content]);
198
+ }, [
199
+ message.id,
200
+ message.parts,
201
+ message.content,
202
+ toolProgress,
203
+ onApprovalResponse,
204
+ ]);
152
205
  // Determine if last thinking group is streaming
153
206
  const lastThinkingGroup = contentItems
154
207
  .filter((item) => item.type === 'thinking')
@@ -236,8 +289,9 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
236
289
  return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
237
290
  }
238
291
  if (item.type === 'text' && item.textPart) {
239
- return (_jsx("div", { className: cn('flex flex-col gap-4', {
292
+ return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
240
293
  'bg-secondary text-secondary-foreground w-fit rounded-2xl px-4 py-2.5': message.role === 'user',
294
+ 'w-full': message.role === 'assistant',
241
295
  }), children: _jsx(Markdown, { markdown: item.textPart.text, proseSize: "sm", className: cn('[&_.prose]:leading-relaxed [&_.prose]:text-current', '[&_.prose_pre]:bg-background/80 [&_.prose_code]:text-current', '[&_.prose_p:last-child]:mb-0 [&_.prose_p]:mb-2', message.role === 'user' &&
242
296
  '[&_.prose_pre]:bg-secondary-foreground/20') }) }, item.id));
243
297
  }
@@ -3,7 +3,18 @@ import type { ChatMessage } from './chat-message';
3
3
  import { type ConversationMeta } from './conversation-history';
4
4
  import { type ModelOption } from './model-selector';
5
5
  import { type ReasoningEffortOption } from './reasoning-effort-selector';
6
+ import { type ApprovalResponseHandler } from './tool-approval-context';
6
7
  type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error';
8
+ /**
9
+ * Progress data for a running tool (command execution with streaming stdout/stderr)
10
+ */
11
+ export interface ToolProgressData {
12
+ toolCallId: string;
13
+ command: string;
14
+ stdout: string;
15
+ stderr: string;
16
+ status: 'running' | 'complete';
17
+ }
7
18
  export interface ChatPanelProps {
8
19
  /** Chat messages */
9
20
  messages: ChatMessage[];
@@ -47,6 +58,10 @@ export interface ChatPanelProps {
47
58
  isExpanded: boolean;
48
59
  /** Toggle expanded state */
49
60
  onExpandedChange: (expanded: boolean) => void;
61
+ /** Whether the panel is minimized (hidden below fold with only flap visible) */
62
+ isMinimized?: boolean;
63
+ /** Toggle minimized state */
64
+ onMinimizedChange?: (minimized: boolean) => void;
50
65
  /** Custom class name */
51
66
  className?: string;
52
67
  /** Error from chat request */
@@ -55,10 +70,14 @@ export interface ChatPanelProps {
55
70
  missingApiKey?: boolean;
56
71
  /** Callback to trigger API key setup */
57
72
  onAddApiKey?: () => void;
73
+ /** Progress data for running tools (streaming stdout/stderr) */
74
+ toolProgress?: Record<string, ToolProgressData>;
75
+ /** Callback for tool approval responses */
76
+ onApprovalResponse?: ApprovalResponseHandler;
58
77
  }
59
78
  /**
60
79
  * Floating chat panel with model selector
61
80
  */
62
- export declare function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder, welcomeMessage, isExpanded, onExpandedChange, className, error, missingApiKey, onAddApiKey, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
81
+ export declare function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder, welcomeMessage, isExpanded, onExpandedChange, isMinimized, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress, onApprovalResponse, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
63
82
  export {};
64
83
  //# sourceMappingURL=chat-panel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat-panel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-panel.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat-panel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-panel.tsx"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { AlertCircle, Minimize2, Plus } from 'lucide-react';
3
+ import { AlertCircle, ChevronDown, MessageCircle, Plus } from 'lucide-react';
4
4
  import { useCallback, useEffect, useRef, useState, } from 'react';
5
5
  import { cn } from '../../lib/utils';
6
6
  import { Button } from '../ui/button';
@@ -11,11 +11,13 @@ import { Messages } from './chat-messages';
11
11
  import { ConversationHistory, } from './conversation-history';
12
12
  import { ModelSelector } from './model-selector';
13
13
  import { ReasoningEffortSelector, } from './reasoning-effort-selector';
14
+ import { ToolApprovalProvider, } from './tool-approval-context';
15
+ import { ToolProgressProvider } from './tool-progress-context';
14
16
  import { AnimatePresence, motion } from 'framer-motion';
15
17
  /**
16
18
  * Floating chat panel with model selector
17
19
  */
18
- export function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, className, error, missingApiKey, onAddApiKey, }) {
20
+ export function ChatPanel({ messages, status, 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, }) {
19
21
  const inputRef = useRef(null);
20
22
  const messagesEndRef = useRef(null);
21
23
  const scrollAreaRef = useRef(null);
@@ -87,9 +89,22 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
87
89
  document.addEventListener('keydown', handleKeyDown);
88
90
  return () => document.removeEventListener('keydown', handleKeyDown);
89
91
  }, [isResponding, isExpanded, handleNewChat]);
90
- return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && (_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(motion.div, { initial: false, animate: {
92
+ // Handle clicking the minimized flap
93
+ const handleFlapClick = useCallback(() => {
94
+ onMinimizedChange?.(false);
95
+ }, [onMinimizedChange]);
96
+ // Handle minimize button - now goes to minimized state
97
+ const handleMinimize = useCallback(() => {
98
+ if (isExpanded) {
99
+ onExpandedChange(false);
100
+ }
101
+ onMinimizedChange?.(true);
102
+ }, [isExpanded, onExpandedChange, onMinimizedChange]);
103
+ 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: {
91
104
  width: isExpanded ? 'min(90vw, 640px)' : '400px',
92
- }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: cn('pointer-events-none fixed bottom-6 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 flex -translate-x-1/2 justify-center', className), children: _jsxs("div", { className: cn('bg-background pointer-events-auto relative w-full border shadow-lg', 'supports-[backdrop-filter]:bg-background/95 backdrop-blur', 'rounded-4xl', isExpanded ? 'overflow-hidden' : 'overflow-visible'), children: [!isExpanded && (_jsx(motion.div, { className: "rounded-4xl pointer-events-none absolute inset-0 z-0", animate: {
105
+ y: isMinimized ? 'calc(100% + 24px)' : 0,
106
+ opacity: isMinimized ? 0 : 1,
107
+ }, transition: { type: 'tween', duration: 0.25, ease: 'easeOut' }, className: cn('pointer-events-none fixed bottom-6 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 flex -translate-x-1/2 justify-center', isMinimized && 'pointer-events-none', className), children: _jsxs("div", { className: cn('bg-background pointer-events-auto relative w-full border shadow-lg', 'supports-[backdrop-filter]:bg-background/95 backdrop-blur', 'rounded-4xl', isExpanded ? 'overflow-hidden' : 'overflow-visible'), children: [!isExpanded && (_jsx(motion.div, { className: "rounded-4xl pointer-events-none absolute inset-0 z-0", animate: {
93
108
  boxShadow: [
94
109
  '0 0 0px 0px color-mix(in oklch, var(--primary) 0%, #ff8800 00%)',
95
110
  '0 0 20px 0px color-mix(in oklch, var(--primary) 20%, #ff8800 30%)',
@@ -107,7 +122,7 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
107
122
  selectedReasoningEffort &&
108
123
  onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: isResponding }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [conversations &&
109
124
  conversations.length > 0 &&
110
- onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, disabled: isResponding })), _jsxs(TooltipProvider, { children: [messages.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleNewChat, disabled: isResponding, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "New chat" }) })] })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => onExpandedChange(false), className: "text-muted-foreground hover:text-foreground", children: _jsx(Minimize2, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] })] })] })] }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-w-0 flex-1 px-2", children: _jsxs("div", { className: "min-w-0 space-y-4 py-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 })), _jsx(Messages, { messages: messages, status: status }), (missingApiKey ||
125
+ onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, disabled: isResponding })), _jsxs(TooltipProvider, { children: [messages.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleNewChat, disabled: isResponding, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "New chat" }) })] })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] })] })] })] }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-w-0 flex-1 px-2", children: _jsxs("div", { className: "min-w-0 max-w-full space-y-4 overflow-hidden py-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 })), _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: _jsx(Messages, { messages: messages, status: status }) }) }), (missingApiKey ||
111
126
  (error &&
112
127
  error.message?.includes('API_KEY not configured'))) && (_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 start chatting with AI" })] }), onAddApiKey && (_jsx(Button, { onClick: onAddApiKey, size: "sm", className: "cursor-pointer", children: "Add API key" }))] })), error &&
113
128
  status === 'error' &&
@@ -4,7 +4,10 @@ export { Messages } from './chat-messages';
4
4
  export { ModelSelector, type ModelOption } from './model-selector';
5
5
  export { ReasoningEffortSelector, type ReasoningEffortOption, } from './reasoning-effort-selector';
6
6
  export { ConversationHistory, type ConversationMeta, } from './conversation-history';
7
- export { ChatPanel, type ChatPanelProps } from './chat-panel';
7
+ export { ChatPanel, type ChatPanelProps, type ToolProgressData, } from './chat-panel';
8
8
  export { ThinkingTimeline, ThinkingTimelineMarker, type ThinkingTimelineItem, type ThinkingTimelineProps, } from './thinking-timeline';
9
9
  export { getToolHandler, DEFAULT_TOOL_HANDLERS, type ToolHandler, } from './tool-handlers';
10
+ export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
11
+ export { ToolApprovalProvider, useToolApprovalResponse, type ApprovalResponseHandler, } from './tool-approval-context';
12
+ export { ToolApproval, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalAccepted, ToolApprovalRejected, ToolApprovalActions, ToolApprovalAction, type ToolApprovalState, } from './tool-approval';
10
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,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,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7D,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,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,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"}
@@ -4,6 +4,9 @@ export { Messages } from './chat-messages';
4
4
  export { ModelSelector } from './model-selector';
5
5
  export { ReasoningEffortSelector, } from './reasoning-effort-selector';
6
6
  export { ConversationHistory, } from './conversation-history';
7
- export { ChatPanel } from './chat-panel';
7
+ export { ChatPanel, } from './chat-panel';
8
8
  export { ThinkingTimeline, ThinkingTimelineMarker, } from './thinking-timeline';
9
9
  export { getToolHandler, DEFAULT_TOOL_HANDLERS, } from './tool-handlers';
10
+ export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
11
+ export { ToolApprovalProvider, useToolApprovalResponse, } from './tool-approval-context';
12
+ export { ToolApproval, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalAccepted, ToolApprovalRejected, ToolApprovalActions, ToolApprovalAction, } from './tool-approval';
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Callback type for responding to tool approval requests
4
+ */
5
+ export type ApprovalResponseHandler = (params: {
6
+ approvalId: string;
7
+ approved: boolean;
8
+ reason?: string;
9
+ }) => void;
10
+ /**
11
+ * Provider for tool approval handling
12
+ */
13
+ export declare function ToolApprovalProvider({ onApprovalResponse, children, }: {
14
+ onApprovalResponse: ApprovalResponseHandler | null;
15
+ children: ReactNode;
16
+ }): import("react/jsx-runtime").JSX.Element;
17
+ /**
18
+ * Hook to get the approval response handler
19
+ */
20
+ export declare function useToolApprovalResponse(): ApprovalResponseHandler | null;
21
+ //# sourceMappingURL=tool-approval-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-approval-context.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-approval-context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA6B,MAAM,OAAO,CAAA;AAEjE;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,KAAK,IAAI,CAAA;AAOV;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,kBAAkB,EAClB,QAAQ,GACT,EAAE;IACD,kBAAkB,EAAE,uBAAuB,GAAG,IAAI,CAAA;IAClD,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAMA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,uBAAuB,GAAG,IAAI,CAExE"}
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ /**
5
+ * Context for tool approval handling
6
+ */
7
+ const ToolApprovalContext = createContext(null);
8
+ /**
9
+ * Provider for tool approval handling
10
+ */
11
+ export function ToolApprovalProvider({ onApprovalResponse, children, }) {
12
+ return (_jsx(ToolApprovalContext.Provider, { value: onApprovalResponse, children: children }));
13
+ }
14
+ /**
15
+ * Hook to get the approval response handler
16
+ */
17
+ export function useToolApprovalResponse() {
18
+ return useContext(ToolApprovalContext);
19
+ }
@@ -0,0 +1,85 @@
1
+ import * as React from 'react';
2
+ import { Button } from '../ui/button';
3
+ /**
4
+ * State of the tool approval
5
+ */
6
+ export type ToolApprovalState = 'approval-requested' | 'approval-responded' | 'output-available' | 'output-denied';
7
+ /**
8
+ * Root component for tool approval UI
9
+ */
10
+ interface ToolApprovalProps {
11
+ children: React.ReactNode;
12
+ /** Current state of the approval */
13
+ state: ToolApprovalState;
14
+ /** Whether the user approved (for responded/output states) */
15
+ approved?: boolean;
16
+ className?: string;
17
+ }
18
+ export declare function ToolApproval({ children, state, approved, className, }: ToolApprovalProps): import("react/jsx-runtime").JSX.Element;
19
+ /**
20
+ * Header section with icon and title
21
+ */
22
+ interface ToolApprovalHeaderProps {
23
+ children: React.ReactNode;
24
+ className?: string;
25
+ }
26
+ export declare function ToolApprovalHeader({ children, className, }: ToolApprovalHeaderProps): import("react/jsx-runtime").JSX.Element;
27
+ /**
28
+ * Content shown only when approval is being requested
29
+ */
30
+ interface ToolApprovalRequestProps {
31
+ children: React.ReactNode;
32
+ className?: string;
33
+ }
34
+ export declare function ToolApprovalRequest({ children, className, }: ToolApprovalRequestProps): import("react/jsx-runtime").JSX.Element | null;
35
+ /**
36
+ * Expandable help section explaining why approval is needed
37
+ */
38
+ interface ToolApprovalHelpProps {
39
+ children: React.ReactNode;
40
+ className?: string;
41
+ }
42
+ export declare function ToolApprovalHelp({ children, className, }: ToolApprovalHelpProps): import("react/jsx-runtime").JSX.Element | null;
43
+ /**
44
+ * Pre-built help content for sandboxing explanation
45
+ */
46
+ export declare function ToolApprovalSandboxHelp({ className }: {
47
+ className?: string;
48
+ }): import("react/jsx-runtime").JSX.Element;
49
+ /**
50
+ * Pre-built help content for dangerous command explanation
51
+ */
52
+ export declare function ToolApprovalDangerousHelp({ className, }: {
53
+ className?: string;
54
+ }): import("react/jsx-runtime").JSX.Element;
55
+ /**
56
+ * Content shown when approval was granted
57
+ */
58
+ interface ToolApprovalAcceptedProps {
59
+ children?: React.ReactNode;
60
+ className?: string;
61
+ }
62
+ export declare function ToolApprovalAccepted({ children, className, }: ToolApprovalAcceptedProps): import("react/jsx-runtime").JSX.Element | null;
63
+ /**
64
+ * Content shown when approval was rejected
65
+ */
66
+ interface ToolApprovalRejectedProps {
67
+ children?: React.ReactNode;
68
+ className?: string;
69
+ }
70
+ export declare function ToolApprovalRejected({ children, className, }: ToolApprovalRejectedProps): import("react/jsx-runtime").JSX.Element | null;
71
+ /**
72
+ * Actions container (approve/reject buttons)
73
+ */
74
+ interface ToolApprovalActionsProps {
75
+ children: React.ReactNode;
76
+ className?: string;
77
+ }
78
+ export declare function ToolApprovalActions({ children, className, }: ToolApprovalActionsProps): import("react/jsx-runtime").JSX.Element | null;
79
+ /**
80
+ * Individual action button
81
+ */
82
+ type ToolApprovalActionProps = React.ComponentProps<typeof Button>;
83
+ export declare function ToolApprovalAction({ className, size, ...props }: ToolApprovalActionProps): import("react/jsx-runtime").JSX.Element;
84
+ export {};
85
+ //# sourceMappingURL=tool-approval.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-approval.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-approval.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,oBAAoB,GACpB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,CAAA;AAenB;;GAEG;AACH,UAAU,iBAAiB;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,oCAAoC;IACpC,KAAK,EAAE,iBAAiB,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,SAAS,GACV,EAAE,iBAAiB,2CAanB;AAED;;GAEG;AACH,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,2CAqBzB;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,SAAS,GACV,EAAE,wBAAwB,kDAQ1B;AAED;;GAEG;AACH,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,SAAS,GACV,EAAE,qBAAqB,kDAkCvB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAc5E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,SAAS,GACV,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,2CAaA;AAED;;GAEG;AACH,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,SAAS,GACV,EAAE,yBAAyB,kDAqB3B;AAED;;GAEG;AACH,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,SAAS,GACV,EAAE,yBAAyB,kDAuB3B;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,SAAS,GACV,EAAE,wBAAwB,kDAkB1B;AAED;;GAEG;AACH,KAAK,uBAAuB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAA;AAElE,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,IAAW,EACX,GAAG,KAAK,EACT,EAAE,uBAAuB,2CAIzB"}
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Check, ChevronDown, HelpCircle, ShieldAlert, X } from 'lucide-react';
4
+ import * as React from 'react';
5
+ import { cn } from '../../lib/utils';
6
+ import { Button } from '../ui/button';
7
+ const ToolApprovalContext = React.createContext({
8
+ state: 'approval-requested',
9
+ });
10
+ function useToolApprovalContext() {
11
+ return React.useContext(ToolApprovalContext);
12
+ }
13
+ export function ToolApproval({ children, state, approved, className, }) {
14
+ return (_jsx(ToolApprovalContext.Provider, { value: { state, approved }, children: _jsx("div", { className: cn('bg-card border-border my-2 overflow-hidden rounded-lg border', className), children: children }) }));
15
+ }
16
+ export function ToolApprovalHeader({ children, className, }) {
17
+ const { state, approved } = useToolApprovalContext();
18
+ // Show different icons based on state
19
+ const icon = state === 'approval-requested' ? (_jsx(ShieldAlert, { className: "size-3.5 shrink-0 text-amber-500" })) : approved ? (_jsx(Check, { className: "text-success size-3.5 shrink-0" })) : (_jsx(X, { className: "text-destructive size-3.5 shrink-0" }));
20
+ return (_jsxs("div", { className: cn('bg-muted/50 flex items-start gap-2 px-3 py-2', className), children: [_jsx("div", { className: "mt-0.5", children: icon }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
21
+ }
22
+ export function ToolApprovalRequest({ children, className, }) {
23
+ const { state } = useToolApprovalContext();
24
+ if (state !== 'approval-requested') {
25
+ return null;
26
+ }
27
+ return _jsx("div", { className: cn('text-xs', className), children: children });
28
+ }
29
+ export function ToolApprovalHelp({ children, className, }) {
30
+ const { state } = useToolApprovalContext();
31
+ const [isOpen, setIsOpen] = React.useState(false);
32
+ // Only show when approval is being requested
33
+ if (state !== 'approval-requested') {
34
+ return null;
35
+ }
36
+ return (_jsxs("div", { className: cn('', className), children: [_jsxs("button", { type: "button", onClick: () => setIsOpen(!isOpen), className: "text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1 text-[10px] transition-colors", children: [_jsx(HelpCircle, { className: "size-2.5" }), _jsx("span", { children: "What does this mean?" }), _jsx(ChevronDown, { className: cn('size-2.5 transition-transform', isOpen && 'rotate-180') })] }), isOpen && (_jsx("div", { className: "bg-muted/30 mt-1.5 rounded-md px-2 py-1.5", children: _jsx("p", { className: "text-muted-foreground text-[10px] leading-relaxed", children: children }) }))] }));
37
+ }
38
+ /**
39
+ * Pre-built help content for sandboxing explanation
40
+ */
41
+ export function ToolApprovalSandboxHelp({ className }) {
42
+ return (_jsxs(ToolApprovalHelp, { className: className, children: [_jsx("strong", { className: "text-foreground", children: "Sandboxing" }), " restricts commands from accessing the network and writing to sensitive locations. Package managers like", ' ', _jsx("code", { className: "bg-muted rounded px-1 py-0.5", children: "pnpm install" }), " need network access to download packages, so they run unsandboxed.", _jsxs("strong", { className: "text-foreground", children: [' ', "Only approve commands you trust."] })] }));
43
+ }
44
+ /**
45
+ * Pre-built help content for dangerous command explanation
46
+ */
47
+ export function ToolApprovalDangerousHelp({ className, }) {
48
+ return (_jsxs(ToolApprovalHelp, { className: className, children: [_jsx("strong", { className: "text-foreground", children: "Dangerous commands" }), " can make irreversible changes to your system, like deleting files recursively or modifying permissions. These commands are flagged based on common patterns (rm -rf, sudo, etc.) to give you a chance to review before execution.", _jsxs("strong", { className: "text-foreground", children: [' ', "Only approve if you understand what the command does."] })] }));
49
+ }
50
+ export function ToolApprovalAccepted({ children, className, }) {
51
+ const { state, approved } = useToolApprovalContext();
52
+ // Show for responded/output states where approved is true
53
+ const shouldShow = (state === 'approval-responded' || state === 'output-available') && approved;
54
+ if (!shouldShow) {
55
+ return null;
56
+ }
57
+ return (_jsx("div", { className: cn('text-success flex items-center gap-1.5 text-xs', className), children: children ?? 'Approved' }));
58
+ }
59
+ export function ToolApprovalRejected({ children, className, }) {
60
+ const { state, approved } = useToolApprovalContext();
61
+ // Show for output-denied or responded with approved=false
62
+ const shouldShow = state === 'output-denied' ||
63
+ ((state === 'approval-responded' || state === 'output-available') &&
64
+ approved === false);
65
+ if (!shouldShow) {
66
+ return null;
67
+ }
68
+ return (_jsx("div", { className: cn('text-destructive flex items-center gap-1.5 text-xs', className), children: children ?? 'Rejected' }));
69
+ }
70
+ export function ToolApprovalActions({ children, className, }) {
71
+ const { state } = useToolApprovalContext();
72
+ // Only show actions when approval is being requested
73
+ if (state !== 'approval-requested') {
74
+ return null;
75
+ }
76
+ return (_jsx("div", { className: cn('border-border flex items-center justify-end gap-2 border-t px-3 py-2', className), children: children }));
77
+ }
78
+ export function ToolApprovalAction({ className, size = 'sm', ...props }) {
79
+ return (_jsx(Button, { className: cn('h-7 text-xs', className), size: size, ...props }));
80
+ }
@@ -1,5 +1,24 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { ThinkingTimelineMarker } from './thinking-timeline';
3
+ import { type ApprovalResponseHandler } from './tool-approval-context';
4
+ /**
5
+ * Progress data for streaming tool execution (e.g., command stdout/stderr)
6
+ */
7
+ export interface ToolStreamingProgress {
8
+ toolCallId: string;
9
+ command?: string;
10
+ stdout: string;
11
+ stderr: string;
12
+ status: 'running' | 'complete';
13
+ }
14
+ /**
15
+ * Approval info passed to renderApproval
16
+ */
17
+ export interface ToolApprovalInfo {
18
+ approvalId: string;
19
+ toolCallId: string;
20
+ args: unknown;
21
+ }
3
22
  /**
4
23
  * Tool handler definition
5
24
  */
@@ -9,6 +28,8 @@ export type ToolHandler = {
9
28
  inline?: boolean;
10
29
  renderOutput: (output: unknown, toolCallId: string) => ReactNode;
11
30
  renderLoading?: (args?: unknown) => ReactNode;
31
+ renderStreaming?: (args: unknown, progress: ToolStreamingProgress) => ReactNode;
32
+ renderApproval?: (approval: ToolApprovalInfo, onRespond: ApprovalResponseHandler) => ReactNode;
12
33
  };
13
34
  /**
14
35
  * Default tool handlers for Moldable tools
@@ -1 +1 @@
1
- {"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAmBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAE5D;;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;CAC9C,CAAA;AA0MD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAm+B7D,CAAA;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAgC5D"}
1
+ {"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAU5D,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;AAkOD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAk1C7D,CAAA;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAmC5D"}
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { BookOpen, Check, CheckCheck, Copy, Download, FileCode, FileText, FolderOpen, Globe, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
3
+ import { BookOpen, Check, CheckCheck, Copy, Download, FileCode, FileText, FolderOpen, Globe, 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';
7
+ import { ToolApproval, ToolApprovalAction, ToolApprovalActions, ToolApprovalDangerousHelp, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalSandboxHelp, } from './tool-approval';
7
8
  /**
8
9
  * Code block component for displaying command output or file contents
9
10
  */
@@ -30,22 +31,22 @@ function summarizeCommand(command) {
30
31
  const firstLine = command.split('\n')[0];
31
32
  // Truncate if too long
32
33
  if (firstLine.length > 60) {
33
- return firstLine.slice(0, 57) + '...';
34
+ return firstLine.slice(0, 25) + '...';
34
35
  }
35
36
  return firstLine;
36
37
  }
37
38
  /**
38
39
  * Terminal output component
39
40
  */
40
- function TerminalOutput({ command, stdout, stderr, exitCode, error, }) {
41
+ function TerminalOutput({ command, stdout, stderr, exitCode, error, sandboxed, }) {
41
42
  const success = !error && (exitCode === 0 || exitCode === undefined);
42
43
  const hasOutput = stdout || stderr || error;
43
- return (_jsxs("div", { className: "border-terminal-border bg-terminal min-w-0 overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "border-terminal-border bg-terminal-header flex min-w-0 items-center gap-2 border-b px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) }), _jsx(CopyButton, { text: command }), success ? (_jsx(Check, { className: "text-success size-3.5 shrink-0" })) : (_jsx(X, { className: "text-terminal-error size-3.5 shrink-0" }))] }), _jsxs("div", { className: "max-h-[300px] overflow-auto p-3", children: [_jsxs("div", { className: "text-terminal-foreground mb-2 break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }), hasOutput && (_jsxs("div", { className: "border-terminal-border/50 border-t pt-2", children: [stdout && (_jsx("pre", { className: "text-terminal-stdout whitespace-pre-wrap break-all font-mono text-xs", children: stdout })), stderr && (_jsx("pre", { className: "text-terminal-stderr whitespace-pre-wrap break-all font-mono text-xs", children: stderr })), error && (_jsx("pre", { className: "text-terminal-error whitespace-pre-wrap break-all font-mono text-xs", children: error }))] }))] })] }));
44
+ return (_jsxs("div", { className: "border-terminal-border bg-terminal min-w-0 max-w-full overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "border-terminal-border bg-terminal-header flex min-w-0 items-center gap-2 border-b px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) }), sandboxed === false && (_jsx("span", { className: "shrink-0 rounded bg-amber-500/20 px-1.5 py-0.5 text-[10px] font-medium text-amber-600", children: "unsandboxed" })), _jsx(CopyButton, { text: command }), success ? (_jsx(Check, { className: "text-success size-3.5 shrink-0" })) : (_jsx(X, { className: "text-terminal-error size-3.5 shrink-0" }))] }), _jsxs("div", { className: "max-h-[300px] overflow-auto p-3", children: [_jsxs("div", { className: "text-terminal-foreground mb-2 break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }), hasOutput && (_jsxs("div", { className: "border-terminal-border/50 border-t pt-2", children: [stdout && (_jsx("pre", { className: "text-terminal-stdout whitespace-pre-wrap break-all font-mono text-xs", children: stdout })), stderr && (_jsx("pre", { className: "text-terminal-stderr whitespace-pre-wrap break-all font-mono text-xs", children: stderr })), error && (_jsx("pre", { className: "text-terminal-error whitespace-pre-wrap break-all font-mono text-xs", children: error }))] }))] })] }));
44
45
  }
45
46
  /**
46
47
  * File operation indicator (inline, minimal)
47
48
  */
48
- function FileOperation({ operation, path, success = true, children, }) {
49
+ function FileOperation({ operation, path, status = 'success', children, }) {
49
50
  const icons = {
50
51
  read: FileText,
51
52
  write: FileText,
@@ -54,7 +55,15 @@ function FileOperation({ operation, path, success = true, children, }) {
54
55
  delete: Trash2,
55
56
  edit: FileCode,
56
57
  };
57
- const labels = {
58
+ const loadingLabels = {
59
+ read: 'Reading',
60
+ write: 'Writing',
61
+ list: 'Listing',
62
+ check: 'Checking',
63
+ delete: 'Deleting',
64
+ edit: 'Editing',
65
+ };
66
+ const completedLabels = {
58
67
  read: 'Read',
59
68
  write: 'Wrote',
60
69
  list: 'Listed',
@@ -64,9 +73,10 @@ function FileOperation({ operation, path, success = true, children, }) {
64
73
  };
65
74
  const Icon = icons[operation];
66
75
  const fileName = getFileName(path);
67
- return (_jsxs("div", { className: "my-1 min-w-0", children: [_jsxs("div", { className: cn('inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs', success
68
- ? 'bg-muted text-muted-foreground'
69
- : 'bg-destructive/10 text-destructive'), children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "shrink-0 font-medium", children: labels[operation] }), _jsx("code", { className: "bg-background/50 min-w-0 truncate rounded px-1 font-mono", title: path, children: fileName }), success ? (_jsx(Check, { className: "size-3 shrink-0 text-green-600" })) : (_jsx(X, { className: "size-3 shrink-0" }))] }), children] }));
76
+ const label = status === 'loading' ? loadingLabels[operation] : completedLabels[operation];
77
+ return (_jsxs("div", { className: "my-1 min-w-0", children: [_jsxs("div", { className: cn('inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs', status === 'error'
78
+ ? 'bg-destructive/10 text-destructive'
79
+ : 'bg-muted text-muted-foreground'), children: [_jsx(Icon, { className: cn('size-3.5 shrink-0', status === 'loading' && 'animate-pulse') }), _jsx("span", { className: "shrink-0 font-medium", children: label }), _jsx("code", { className: "bg-background/50 min-w-0 truncate rounded px-1 font-mono", title: path, children: fileName }), status === 'success' && (_jsx(Check, { className: "size-3 shrink-0 text-green-600" })), status === 'error' && _jsx(X, { className: "size-3 shrink-0" }), status === 'loading' && (_jsx("span", { className: "text-muted-foreground/60 shrink-0", children: "..." }))] }), children] }));
70
80
  }
71
81
  /**
72
82
  * Extract just the filename from a path
@@ -83,14 +93,17 @@ export const DEFAULT_TOOL_HANDLERS = {
83
93
  loadingLabel: 'Reading file...',
84
94
  marker: ThinkingTimelineMarker.File,
85
95
  inline: true,
86
- renderLoading: () => (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FileText, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Reading file..." })] })),
96
+ renderLoading: (args) => {
97
+ const { path } = (args ?? {});
98
+ return (_jsx(FileOperation, { operation: "read", path: path || 'file', status: "loading" }));
99
+ },
87
100
  renderOutput: (output, toolCallId) => {
88
101
  const result = (output ?? {});
89
- if (!result.success) {
90
- return (_jsx(FileOperation, { operation: "read", path: result.path || 'file', success: false }, toolCallId));
102
+ // If output is empty, tool is still executing
103
+ if (output === undefined || output === null) {
104
+ return (_jsx(FileOperation, { operation: "read", path: "file", status: "loading" }, toolCallId));
91
105
  }
92
- // Show simple indicator for successful reads
93
- return (_jsx(FileOperation, { operation: "read", path: result.path || 'file', success: true }, toolCallId));
106
+ return (_jsx(FileOperation, { operation: "read", path: result.path || 'file', status: result.success === false ? 'error' : 'success' }, toolCallId));
94
107
  },
95
108
  },
96
109
  writeFile: {
@@ -108,26 +121,33 @@ export const DEFAULT_TOOL_HANDLERS = {
108
121
  return (_jsxs("div", { className: "my-1 min-w-0", children: [_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FileText, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "shrink-0 font-medium", children: "Writing" }), _jsx("code", { className: "bg-background/50 min-w-0 truncate rounded px-1 font-mono", children: path ? getFileName(path) : 'file' }), _jsxs("span", { className: "text-muted-foreground/70 shrink-0", children: ["(", lineCount, " line", lineCount !== 1 ? 's' : '', ")"] })] }), _jsx("div", { className: "mt-2", children: _jsx(CodeBlock, { maxHeight: 200, children: preview }) })] }));
109
122
  }
110
123
  // Fallback when content hasn't started streaming yet
111
- return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FileText, { className: "size-3.5 shrink-0 animate-pulse" }), _jsxs("span", { className: "truncate", children: ["Writing ", path ? getFileName(path) : 'file', "..."] })] }));
124
+ return (_jsx(FileOperation, { operation: "write", path: path || 'file', status: "loading" }));
112
125
  },
113
126
  renderOutput: (output, toolCallId) => {
114
127
  const result = (output ?? {});
115
- if (!result.success) {
116
- return (_jsx(FileOperation, { operation: "write", path: result.path || 'file', success: false }, toolCallId));
128
+ // If output is empty, tool is still executing
129
+ if (output === undefined || output === null) {
130
+ return (_jsx(FileOperation, { operation: "write", path: "file", status: "loading" }, toolCallId));
117
131
  }
118
- // Show simple indicator for successful writes
119
- return (_jsx(FileOperation, { operation: "write", path: result.path || 'file', success: true }, toolCallId));
132
+ return (_jsx(FileOperation, { operation: "write", path: result.path || 'file', status: result.success === false ? 'error' : 'success' }, toolCallId));
120
133
  },
121
134
  },
122
135
  listDirectory: {
123
136
  loadingLabel: 'Listing directory...',
124
137
  marker: ThinkingTimelineMarker.Folder,
125
138
  inline: true,
126
- renderLoading: () => (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FolderOpen, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Listing directory..." })] })),
139
+ renderLoading: (args) => {
140
+ const { path } = (args ?? {});
141
+ return (_jsx(FileOperation, { operation: "list", path: path || 'directory', status: "loading" }));
142
+ },
127
143
  renderOutput: (output, toolCallId) => {
128
144
  const result = (output ?? {});
145
+ // If output is empty, tool is still executing
146
+ if (output === undefined || output === null) {
147
+ return (_jsx(FileOperation, { operation: "list", path: "directory", status: "loading" }, toolCallId));
148
+ }
129
149
  if (result.success === false) {
130
- return (_jsx(FileOperation, { operation: "list", path: result.path || 'directory', success: false }, toolCallId));
150
+ return (_jsx(FileOperation, { operation: "list", path: result.path || 'directory', status: "error" }, toolCallId));
131
151
  }
132
152
  // Format entries for display
133
153
  const entries = result.items
@@ -140,10 +160,17 @@ export const DEFAULT_TOOL_HANDLERS = {
140
160
  loadingLabel: 'Checking file...',
141
161
  marker: ThinkingTimelineMarker.File,
142
162
  inline: true,
143
- renderLoading: () => (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FileText, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Checking file..." })] })),
163
+ renderLoading: (args) => {
164
+ const { path } = (args ?? {});
165
+ return (_jsx(FileOperation, { operation: "check", path: path || 'file', status: "loading" }));
166
+ },
144
167
  renderOutput: (output, toolCallId) => {
145
168
  const result = (output ?? {});
146
- 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(FileText, { className: "size-3.5 shrink-0" }), _jsx("code", { className: "bg-background/50 min-w-0 truncate rounded px-1 font-mono", children: result.path || 'file' }), _jsx("span", { className: "shrink-0", children: result.exists ? 'exists' : 'not found' }), result.exists ? (_jsx(Check, { className: "size-3 shrink-0 text-green-600" })) : (_jsx(X, { className: "size-3 shrink-0 text-amber-500" }))] }, toolCallId));
169
+ // If output is empty, tool is still executing
170
+ if (output === undefined || output === null) {
171
+ return (_jsx(FileOperation, { operation: "check", path: "file", status: "loading" }, toolCallId));
172
+ }
173
+ 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(FileText, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "shrink-0 font-medium", children: "Checked" }), _jsx("code", { className: "bg-background/50 min-w-0 truncate rounded px-1 font-mono", title: result.path, children: getFileName(result.path || 'file') }), _jsx("span", { className: "shrink-0", children: result.exists ? 'exists' : 'not found' }), result.exists ? (_jsx(Check, { className: "size-3 shrink-0 text-green-600" })) : (_jsx(X, { className: "size-3 shrink-0 text-amber-500" }))] }, toolCallId));
147
174
  },
148
175
  },
149
176
  executeBashCommand: {
@@ -154,14 +181,18 @@ export const DEFAULT_TOOL_HANDLERS = {
154
181
  const { command } = (args ?? {});
155
182
  // Show streaming command as it's being written
156
183
  if (command && command.trim()) {
157
- return (_jsxs("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) })] }), _jsx("div", { className: "max-h-[150px] overflow-auto p-3", children: _jsxs("div", { className: "text-terminal-foreground break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }) })] }));
184
+ return (_jsxs("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) }), _jsx("span", { className: "text-terminal-muted shrink-0 text-[10px]", children: "Running..." })] }), _jsx("div", { className: "max-h-[150px] overflow-auto p-3", children: _jsxs("div", { className: "text-terminal-foreground break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }) })] }));
158
185
  }
159
186
  // Fallback when command hasn't started streaming
160
- return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Generating command..." })] }) }));
187
+ return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Generating command..." })] }) }));
161
188
  },
162
189
  renderOutput: (output, toolCallId) => {
163
190
  const result = (output ?? {});
164
- return (_jsx("div", { className: "my-2 min-w-0", children: _jsx(TerminalOutput, { command: result.command || 'command', stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, error: result.error }) }, toolCallId));
191
+ // If output is empty, tool is still executing
192
+ if (output === undefined || output === null) {
193
+ return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Executing..." })] }) }, toolCallId));
194
+ }
195
+ return (_jsx("div", { className: "my-2 min-w-0 max-w-full", children: _jsx(TerminalOutput, { command: result.command || 'command', stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, error: result.error }) }, toolCallId));
165
196
  },
166
197
  },
167
198
  runCommand: {
@@ -169,17 +200,48 @@ export const DEFAULT_TOOL_HANDLERS = {
169
200
  marker: ThinkingTimelineMarker.Terminal,
170
201
  inline: true,
171
202
  renderLoading: (args) => {
172
- const { command } = (args ?? {});
203
+ const { command, sandbox } = (args ?? {});
173
204
  // Show streaming command as it's being written
174
205
  if (command && command.trim()) {
175
- return (_jsxs("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) })] }), _jsx("div", { className: "max-h-[150px] overflow-auto p-3", children: _jsxs("div", { className: "text-terminal-foreground break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }) })] }));
206
+ return (_jsxs("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: summarizeCommand(command) }), sandbox === false && (_jsx("span", { className: "shrink-0 rounded bg-amber-500/20 px-1.5 py-0.5 text-[10px] font-medium text-amber-600", children: "unsandboxed" })), _jsx("span", { className: "text-terminal-muted shrink-0 text-[10px]", children: "Running..." })] }), _jsx("div", { className: "max-h-[150px] overflow-auto p-3", children: _jsxs("div", { className: "text-terminal-foreground break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }) })] }));
176
207
  }
177
208
  // Fallback when command hasn't started streaming
178
- return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Preparing command..." })] }) }));
209
+ return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Preparing command..." })] }) }));
210
+ },
211
+ // Render with streaming stdout/stderr output
212
+ renderStreaming: (args, progress) => {
213
+ const { command, sandbox } = (args ?? {});
214
+ const { stdout, stderr, status } = progress;
215
+ const hasOutput = stdout || stderr;
216
+ const isComplete = status === 'complete';
217
+ return (_jsxs("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: [_jsxs("div", { className: "border-terminal-border bg-terminal-header flex min-w-0 items-center gap-2 border-b px-3 py-1.5", children: [_jsx(Terminal, { className: cn('size-3.5 shrink-0', isComplete
218
+ ? 'text-terminal-muted'
219
+ : 'text-terminal-muted animate-pulse') }), _jsx("code", { className: "text-terminal-foreground min-w-0 flex-1 truncate font-mono text-xs", children: command ? summarizeCommand(command) : 'Running...' }), sandbox === false && (_jsx("span", { className: "shrink-0 rounded bg-amber-500/20 px-1.5 py-0.5 text-[10px] font-medium text-amber-600", children: "unsandboxed" })), isComplete ? (_jsx(Check, { className: "text-terminal-stdout size-3.5 shrink-0" })) : (_jsx("span", { className: "text-terminal-muted shrink-0 animate-pulse text-[10px]", children: "Running..." }))] }), _jsxs("div", { className: "max-h-[300px] overflow-auto p-3", children: [_jsxs("div", { className: "text-terminal-foreground mb-2 break-all font-mono text-xs", children: [_jsx("span", { className: "text-terminal-muted", children: "$" }), " ", command] }), hasOutput && (_jsxs("div", { className: "border-terminal-border/50 border-t pt-2", children: [stdout && (_jsx("pre", { className: "text-terminal-stdout whitespace-pre-wrap break-all font-mono text-xs", children: stdout })), stderr && (_jsx("pre", { className: "text-terminal-stderr whitespace-pre-wrap break-all font-mono text-xs", children: stderr }))] }))] })] }));
179
220
  },
180
221
  renderOutput: (output, toolCallId) => {
181
222
  const result = (output ?? {});
182
- return (_jsx("div", { className: "my-2 min-w-0", children: _jsx(TerminalOutput, { command: result.command || 'command', stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, error: result.error }) }, toolCallId));
223
+ // If output is empty, tool is still executing
224
+ if (output === undefined || output === null) {
225
+ return (_jsx("div", { className: "border-terminal-border bg-terminal my-2 min-w-0 max-w-full overflow-hidden rounded-lg border", children: _jsxs("div", { className: "bg-terminal-header flex min-w-0 items-center gap-2 px-3 py-1.5", children: [_jsx(Terminal, { className: "text-terminal-muted size-3.5 shrink-0 animate-pulse" }), _jsx("code", { className: "text-terminal-foreground/60 min-w-0 flex-1 truncate font-mono text-xs italic", children: "Executing..." })] }) }, toolCallId));
226
+ }
227
+ return (_jsx("div", { className: "my-2 min-w-0 max-w-full", children: _jsx(TerminalOutput, { command: result.command || 'command', stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, error: result.error, sandboxed: result.sandboxed }) }, toolCallId));
228
+ },
229
+ // Render approval request for unsandboxed or dangerous commands
230
+ renderApproval: (approval, onRespond) => {
231
+ const { command, sandbox } = (approval.args ?? {});
232
+ // Determine the reason for approval
233
+ const isUnsandboxed = sandbox === false;
234
+ const approvalReason = isUnsandboxed
235
+ ? 'This command requires running outside the sandbox.'
236
+ : 'This command has been flagged as potentially dangerous.';
237
+ return (_jsxs(ToolApproval, { state: "approval-requested", children: [_jsx(ToolApprovalHeader, { children: _jsxs(ToolApprovalRequest, { children: [_jsx("div", { className: "mb-1 text-xs font-medium", children: "Command requires approval" }), _jsx("div", { className: "text-muted-foreground mb-2 text-[10px]", children: approvalReason }), _jsx("code", { className: "bg-muted block rounded px-2 py-1.5 font-mono text-[10px]", children: command }), isUnsandboxed ? (_jsx(ToolApprovalSandboxHelp, { className: "mt-2" })) : (_jsx(ToolApprovalDangerousHelp, { className: "mt-2" }))] }) }), _jsxs(ToolApprovalActions, { children: [_jsx(ToolApprovalAction, { variant: "outline", onClick: () => onRespond({
238
+ approvalId: approval.approvalId,
239
+ approved: false,
240
+ reason: 'User rejected command execution',
241
+ }), children: "Reject" }), _jsx(ToolApprovalAction, { onClick: () => onRespond({
242
+ approvalId: approval.approvalId,
243
+ approved: true,
244
+ }), children: "Approve" })] })] }));
183
245
  },
184
246
  },
185
247
  // ─────────────────────────────────────────────────────────────────────────────
@@ -191,11 +253,15 @@ export const DEFAULT_TOOL_HANDLERS = {
191
253
  inline: true,
192
254
  renderLoading: (args) => {
193
255
  const { path } = (args ?? {});
194
- return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Trash2, { className: "size-3.5 shrink-0 animate-pulse" }), _jsxs("span", { className: "truncate", children: ["Deleting ", path ? getFileName(path) : 'file', "..."] })] }));
256
+ return (_jsx(FileOperation, { operation: "delete", path: path || 'file', status: "loading" }));
195
257
  },
196
258
  renderOutput: (output, toolCallId) => {
197
259
  const result = (output ?? {});
198
- return (_jsx(FileOperation, { operation: "delete", path: result.path || 'file', success: result.success !== false }, toolCallId));
260
+ // If output is empty, tool is still executing
261
+ if (output === undefined || output === null) {
262
+ return (_jsx(FileOperation, { operation: "delete", path: "file", status: "loading" }, toolCallId));
263
+ }
264
+ return (_jsx(FileOperation, { operation: "delete", path: result.path || 'file', status: result.success === false ? 'error' : 'success' }, toolCallId));
199
265
  },
200
266
  },
201
267
  editFile: {
@@ -204,11 +270,15 @@ export const DEFAULT_TOOL_HANDLERS = {
204
270
  inline: true,
205
271
  renderLoading: (args) => {
206
272
  const { path } = (args ?? {});
207
- return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(FileCode, { className: "size-3.5 shrink-0 animate-pulse" }), _jsxs("span", { className: "truncate", children: ["Editing ", path ? getFileName(path) : 'file', "..."] })] }));
273
+ return (_jsx(FileOperation, { operation: "edit", path: path || 'file', status: "loading" }));
208
274
  },
209
275
  renderOutput: (output, toolCallId) => {
210
276
  const result = (output ?? {});
211
- return (_jsx(FileOperation, { operation: "edit", path: result.path || 'file', success: result.success !== false }, toolCallId));
277
+ // If output is empty, tool is still executing
278
+ if (output === undefined || output === null) {
279
+ return (_jsx(FileOperation, { operation: "edit", path: "file", status: "loading" }, toolCallId));
280
+ }
281
+ return (_jsx(FileOperation, { operation: "edit", path: result.path || 'file', status: result.success === false ? 'error' : 'success' }, toolCallId));
212
282
  },
213
283
  },
214
284
  // ─────────────────────────────────────────────────────────────────────────────
@@ -226,6 +296,10 @@ export const DEFAULT_TOOL_HANDLERS = {
226
296
  },
227
297
  renderOutput: (output, toolCallId) => {
228
298
  const result = (output ?? {});
299
+ // If output is empty, tool is still executing
300
+ if (output === undefined || output === null) {
301
+ 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(Search, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Searching..." })] }, toolCallId));
302
+ }
229
303
  // Handle raw content output (ripgrep format)
230
304
  if (result.content && !result.matches) {
231
305
  const lines = result.content.split('\n').filter(Boolean);
@@ -256,6 +330,10 @@ export const DEFAULT_TOOL_HANDLERS = {
256
330
  },
257
331
  renderOutput: (output, toolCallId) => {
258
332
  const result = (output ?? {});
333
+ // If output is empty, tool is still executing
334
+ if (output === undefined || output === null) {
335
+ 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(Search, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Finding files..." })] }, toolCallId));
336
+ }
259
337
  if (result.success === false || !result.files?.length) {
260
338
  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(Search, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: "No files found" })] }, toolCallId));
261
339
  }
@@ -277,6 +355,10 @@ export const DEFAULT_TOOL_HANDLERS = {
277
355
  },
278
356
  renderOutput: (output, toolCallId) => {
279
357
  const result = (output ?? {});
358
+ // If output is empty, tool is still executing
359
+ if (output === undefined || output === null) {
360
+ 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(Globe, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Searching the web..." })] }, toolCallId));
361
+ }
280
362
  if (result.success === false || !result.results?.length) {
281
363
  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(Globe, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: "No results found" })] }, toolCallId));
282
364
  }
@@ -378,7 +460,36 @@ export const DEFAULT_TOOL_HANDLERS = {
378
460
  return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 min-w-0", children: [_jsxs("div", { className: "inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(BookOpen, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "font-medium", children: "Skills config initialized" }), _jsx(Check, { className: "size-3 shrink-0 text-green-600" })] }), result.repositories && result.repositories.length > 0 && (_jsxs("div", { className: "text-muted-foreground px-2 pb-1 text-[10px]", children: ["Added: ", result.repositories.map((r) => r.name).join(', ')] }))] }, toolCallId));
379
461
  },
380
462
  },
463
+ // ─────────────────────────────────────────────────────────────────────────────
464
+ // App Scaffolding
465
+ // ─────────────────────────────────────────────────────────────────────────────
466
+ scaffoldApp: {
467
+ loadingLabel: 'Creating app...',
468
+ marker: ThinkingTimelineMarker.Default,
469
+ inline: true,
470
+ renderLoading: (args) => {
471
+ const { name, appId } = (args ?? {});
472
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Package, { className: "size-3.5 shrink-0 animate-pulse" }), _jsxs("span", { className: "truncate", children: ["Creating ", name || appId || 'app', "..."] })] }));
473
+ },
474
+ renderOutput: (output, toolCallId) => {
475
+ const result = (output ?? {});
476
+ // If output is empty, tool is still executing
477
+ if (output === undefined || output === null) {
478
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Package, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: "Creating app..." })] }, toolCallId));
479
+ }
480
+ if (result.success === false) {
481
+ return (_jsxs("div", { className: "bg-destructive/10 text-destructive my-1 inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Package, { className: "size-3.5 shrink-0" }), _jsx("span", { className: "truncate", children: result.error || 'Failed to create app' })] }, toolCallId));
482
+ }
483
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground my-1 min-w-0 rounded-md", children: [_jsxs("div", { className: "flex items-center gap-2 px-2 py-1.5 text-xs", children: [_jsx(Package, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "font-medium", children: [result.icon, " ", result.name || result.appId] }), _jsx(Check, { className: "size-3 shrink-0 text-green-600" })] }), _jsxs("div", { className: "border-border/50 flex flex-wrap gap-x-3 gap-y-0.5 border-t px-2 py-1 text-[10px]", children: [result.port && (_jsxs("span", { children: ["Port: ", _jsx("code", { className: "font-mono", children: result.port })] })), result.pnpmInstalled && (_jsx("span", { className: "text-green-600", children: "deps installed" })), result.registered && (_jsx("span", { className: "text-green-600", children: "registered" })), result.files && (_jsxs("span", { className: "text-muted-foreground/70", children: [result.files.length, " files"] }))] })] }, toolCallId));
484
+ },
485
+ },
381
486
  };
487
+ /**
488
+ * Loading indicator for inline tool loading states
489
+ */
490
+ function LoadingIndicator({ icon: Icon, children, }) {
491
+ return (_jsxs("div", { className: "bg-muted text-muted-foreground inline-flex max-w-full items-center gap-2 rounded-md px-2 py-1 text-xs", children: [_jsx(Icon, { className: "size-3.5 shrink-0 animate-pulse" }), _jsx("span", { className: "truncate", children: children })] }));
492
+ }
382
493
  /**
383
494
  * Get the handler for a tool
384
495
  */
@@ -389,9 +500,10 @@ export function getToolHandler(toolName) {
389
500
  }
390
501
  // Default handler for unknown tools
391
502
  return {
392
- loadingLabel: `Using ${toolName}...`,
503
+ loadingLabel: `Running ${toolName}...`,
393
504
  marker: ThinkingTimelineMarker.Default,
394
505
  inline: false,
506
+ renderLoading: () => (_jsxs(LoadingIndicator, { icon: Sparkles, children: ["Running ", toolName, "..."] })),
395
507
  renderOutput: (output, toolCallId) => {
396
508
  const outputContent = typeof output === 'string'
397
509
  ? output
@@ -0,0 +1,27 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Progress data for a running tool (command execution with streaming stdout/stderr)
4
+ */
5
+ export interface ToolProgressData {
6
+ toolCallId: string;
7
+ command: string;
8
+ stdout: string;
9
+ stderr: string;
10
+ status: 'running' | 'complete';
11
+ }
12
+ /**
13
+ * Provider for tool progress data
14
+ */
15
+ export declare function ToolProgressProvider({ value, children, }: {
16
+ value: Record<string, ToolProgressData>;
17
+ children: ReactNode;
18
+ }): import("react/jsx-runtime").JSX.Element;
19
+ /**
20
+ * Hook to get tool progress data
21
+ */
22
+ export declare function useToolProgress(): Record<string, ToolProgressData>;
23
+ /**
24
+ * Hook to get progress for a specific tool call
25
+ */
26
+ export declare function useToolCallProgress(toolCallId: string | undefined): ToolProgressData | undefined;
27
+ //# sourceMappingURL=tool-progress-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-progress-context.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-progress-context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA6B,MAAM,OAAO,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAC/B;AAOD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACvC,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAMA;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAElE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,gBAAgB,GAAG,SAAS,CAG9B"}
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext } from 'react';
4
+ /**
5
+ * Context for tool progress data
6
+ */
7
+ const ToolProgressContext = createContext({});
8
+ /**
9
+ * Provider for tool progress data
10
+ */
11
+ export function ToolProgressProvider({ value, children, }) {
12
+ return (_jsx(ToolProgressContext.Provider, { value: value, children: children }));
13
+ }
14
+ /**
15
+ * Hook to get tool progress data
16
+ */
17
+ export function useToolProgress() {
18
+ return useContext(ToolProgressContext);
19
+ }
20
+ /**
21
+ * Hook to get progress for a specific tool call
22
+ */
23
+ export function useToolCallProgress(toolCallId) {
24
+ const progress = useContext(ToolProgressContext);
25
+ return toolCallId ? progress[toolCallId] : undefined;
26
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { cn } from './lib/utils';
2
2
  export { ThemeProvider, useTheme, themeScript, type Theme } from './lib/theme';
3
3
  export { WorkspaceProvider, useWorkspace, WORKSPACE_HEADER, } from './lib/workspace';
4
- export { useMoldableCommands, useMoldableCommand, isInMoldable, sendToMoldable, type AppCommand, type CommandAction, type CommandsResponse, type CommandMessage, } from './lib/commands';
4
+ export { useMoldableCommands, useMoldableCommand, isInMoldable, sendToMoldable, downloadFile, type AppCommand, type CommandAction, type CommandsResponse, type CommandMessage, type DownloadFileOptions, } from './lib/commands';
5
5
  export * from './components/ui';
6
6
  export { useIsMobile } from './hooks/use-mobile';
7
7
  export { Markdown } from './components/markdown';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAGhC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AAG9E,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAA;AAGvB,cAAc,iBAAiB,CAAA;AAG/B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAGhD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAGzD,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAGhC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AAG9E,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAA;AAGvB,cAAc,iBAAiB,CAAA;AAG/B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAGhD,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAGzD,cAAc,mBAAmB,CAAA"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { ThemeProvider, useTheme, themeScript } from './lib/theme';
5
5
  // Export workspace
6
6
  export { WorkspaceProvider, useWorkspace, WORKSPACE_HEADER, } from './lib/workspace';
7
7
  // Export commands
8
- export { useMoldableCommands, useMoldableCommand, isInMoldable, sendToMoldable, } from './lib/commands';
8
+ export { useMoldableCommands, useMoldableCommand, isInMoldable, sendToMoldable, downloadFile, } from './lib/commands';
9
9
  // Export UI components
10
10
  export * from './components/ui';
11
11
  // Export hooks
@@ -71,4 +71,41 @@ export declare function sendToMoldable(message: {
71
71
  type: string;
72
72
  [key: string]: unknown;
73
73
  }): void;
74
+ /**
75
+ * Options for downloading a file
76
+ */
77
+ export interface DownloadFileOptions {
78
+ /** Suggested filename for the save dialog */
79
+ filename: string;
80
+ /** File content - either a string or base64-encoded data */
81
+ data: string;
82
+ /** MIME type of the file (e.g., 'text/csv', 'application/json') */
83
+ mimeType: string;
84
+ /** If true, data is base64-encoded binary; if false, data is plain text */
85
+ isBase64?: boolean;
86
+ }
87
+ /**
88
+ * Trigger a file download via Moldable's native save dialog.
89
+ * Works inside Moldable's iframe environment where browser downloads don't work.
90
+ *
91
+ * @example
92
+ * ```tsx
93
+ * // Export CSV
94
+ * downloadFile({
95
+ * filename: 'data.csv',
96
+ * data: 'name,value\nfoo,1\nbar,2',
97
+ * mimeType: 'text/csv',
98
+ * })
99
+ *
100
+ * // Export JSON
101
+ * downloadFile({
102
+ * filename: 'data.json',
103
+ * data: JSON.stringify({ items: [...] }, null, 2),
104
+ * mimeType: 'application/json',
105
+ * })
106
+ * ```
107
+ *
108
+ * @returns Promise that resolves when the save dialog completes (or rejects on error)
109
+ */
110
+ export declare function downloadFile(options: DownloadFileOptions): Promise<void>;
74
111
  //# sourceMappingURL=commands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/lib/commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gDAAgD;IAChD,MAAM,EAAE,aAAa,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,UAAU,EAAE,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,QAwBtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,QAgBrC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAGtC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,QAMA"}
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/lib/commands.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAErC;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;IACb,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gDAAgD;IAChD,MAAM,EAAE,aAAa,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,UAAU,EAAE,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAA;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,QAwBtD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,QAgBrC;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAGtC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,QAMA;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAA;IAChB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DxE"}
@@ -71,3 +71,88 @@ export function sendToMoldable(message) {
71
71
  }
72
72
  window.parent.postMessage(message, '*');
73
73
  }
74
+ /**
75
+ * Trigger a file download via Moldable's native save dialog.
76
+ * Works inside Moldable's iframe environment where browser downloads don't work.
77
+ *
78
+ * @example
79
+ * ```tsx
80
+ * // Export CSV
81
+ * downloadFile({
82
+ * filename: 'data.csv',
83
+ * data: 'name,value\nfoo,1\nbar,2',
84
+ * mimeType: 'text/csv',
85
+ * })
86
+ *
87
+ * // Export JSON
88
+ * downloadFile({
89
+ * filename: 'data.json',
90
+ * data: JSON.stringify({ items: [...] }, null, 2),
91
+ * mimeType: 'application/json',
92
+ * })
93
+ * ```
94
+ *
95
+ * @returns Promise that resolves when the save dialog completes (or rejects on error)
96
+ */
97
+ export function downloadFile(options) {
98
+ return new Promise((resolve, reject) => {
99
+ if (!isInMoldable()) {
100
+ // Fallback for browser: use traditional blob download
101
+ try {
102
+ const blob = options.isBase64
103
+ ? new Blob([Uint8Array.from(atob(options.data), (c) => c.charCodeAt(0))], {
104
+ type: options.mimeType,
105
+ })
106
+ : new Blob([options.data], { type: options.mimeType });
107
+ const url = URL.createObjectURL(blob);
108
+ const a = document.createElement('a');
109
+ a.href = url;
110
+ a.download = options.filename;
111
+ document.body.appendChild(a);
112
+ a.click();
113
+ document.body.removeChild(a);
114
+ URL.revokeObjectURL(url);
115
+ resolve();
116
+ }
117
+ catch (err) {
118
+ reject(err);
119
+ }
120
+ return;
121
+ }
122
+ // Generate a unique ID for this download request
123
+ const requestId = `download-${Date.now()}-${Math.random().toString(36).slice(2)}`;
124
+ // Listen for the response
125
+ const handleResponse = (event) => {
126
+ if (event.data?.type !== 'moldable:save-file-result')
127
+ return;
128
+ if (event.data?.requestId !== requestId)
129
+ return;
130
+ window.removeEventListener('message', handleResponse);
131
+ if (event.data.success) {
132
+ resolve();
133
+ }
134
+ else if (event.data.cancelled) {
135
+ // User cancelled - not an error, just resolve
136
+ resolve();
137
+ }
138
+ else {
139
+ reject(new Error(event.data.error || 'Download failed'));
140
+ }
141
+ };
142
+ window.addEventListener('message', handleResponse);
143
+ // Send the download request to Moldable
144
+ sendToMoldable({
145
+ type: 'moldable:save-file',
146
+ requestId,
147
+ filename: options.filename,
148
+ data: options.data,
149
+ mimeType: options.mimeType,
150
+ isBase64: options.isBase64 ?? false,
151
+ });
152
+ // Timeout after 5 minutes (user might take time in save dialog)
153
+ setTimeout(() => {
154
+ window.removeEventListener('message', handleResponse);
155
+ reject(new Error('Download timed out'));
156
+ }, 5 * 60 * 1000);
157
+ });
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moldable-ai/ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Shared UI components for Moldable applications",
5
5
  "author": "Desiderata LLC",
6
6
  "license": "Elastic-2.0",