@moldable-ai/ui 0.2.2 → 0.2.5

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 (32) hide show
  1. package/dist/components/chat/chat-message.d.ts +4 -0
  2. package/dist/components/chat/chat-message.d.ts.map +1 -1
  3. package/dist/components/chat/chat-message.js +64 -10
  4. package/dist/components/chat/chat-panel.d.ts +20 -1
  5. package/dist/components/chat/chat-panel.d.ts.map +1 -1
  6. package/dist/components/chat/chat-panel.js +20 -5
  7. package/dist/components/chat/index.d.ts +4 -1
  8. package/dist/components/chat/index.d.ts.map +1 -1
  9. package/dist/components/chat/index.js +4 -1
  10. package/dist/components/chat/tool-approval-context.d.ts +21 -0
  11. package/dist/components/chat/tool-approval-context.d.ts.map +1 -0
  12. package/dist/components/chat/tool-approval-context.js +19 -0
  13. package/dist/components/chat/tool-approval.d.ts +85 -0
  14. package/dist/components/chat/tool-approval.d.ts.map +1 -0
  15. package/dist/components/chat/tool-approval.js +80 -0
  16. package/dist/components/chat/tool-handlers.d.ts +21 -0
  17. package/dist/components/chat/tool-handlers.d.ts.map +1 -1
  18. package/dist/components/chat/tool-handlers.js +147 -35
  19. package/dist/components/chat/tool-progress-context.d.ts +27 -0
  20. package/dist/components/chat/tool-progress-context.d.ts.map +1 -0
  21. package/dist/components/chat/tool-progress-context.js +26 -0
  22. package/dist/components/ui/item.d.ts +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/lib/commands.d.ts +37 -0
  27. package/dist/lib/commands.d.ts.map +1 -1
  28. package/dist/lib/commands.js +85 -0
  29. package/package.json +14 -14
  30. package/dist/components/chat/markdown.d.ts +0 -9
  31. package/dist/components/chat/markdown.d.ts.map +0 -1
  32. package/dist/components/chat/markdown.js +0 -47
@@ -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
+ }
@@ -11,7 +11,7 @@ declare function Item({ className, variant, size, asChild, ...props }: React.Com
11
11
  asChild?: boolean;
12
12
  }): import("react/jsx-runtime").JSX.Element;
13
13
  declare const itemMediaVariants: (props?: ({
14
- variant?: "image" | "default" | "icon" | null | undefined;
14
+ variant?: "default" | "image" | "icon" | null | undefined;
15
15
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
16
16
  declare function ItemMedia({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>): import("react/jsx-runtime").JSX.Element;
17
17
  declare function ItemContent({ className, ...props }: React.ComponentProps<'div'>): import("react/jsx-runtime").JSX.Element;
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,CAqExE"}
@@ -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
+ }