@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.
- package/dist/components/chat/chat-message.d.ts +4 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +64 -10
- package/dist/components/chat/chat-panel.d.ts +20 -1
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +20 -5
- package/dist/components/chat/index.d.ts +4 -1
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +4 -1
- package/dist/components/chat/tool-approval-context.d.ts +21 -0
- package/dist/components/chat/tool-approval-context.d.ts.map +1 -0
- package/dist/components/chat/tool-approval-context.js +19 -0
- package/dist/components/chat/tool-approval.d.ts +85 -0
- package/dist/components/chat/tool-approval.d.ts.map +1 -0
- package/dist/components/chat/tool-approval.js +80 -0
- package/dist/components/chat/tool-handlers.d.ts +21 -0
- package/dist/components/chat/tool-handlers.d.ts.map +1 -1
- package/dist/components/chat/tool-handlers.js +147 -35
- package/dist/components/chat/tool-progress-context.d.ts +27 -0
- package/dist/components/chat/tool-progress-context.d.ts.map +1 -0
- package/dist/components/chat/tool-progress-context.js +26 -0
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/commands.d.ts +37 -0
- package/dist/lib/commands.d.ts.map +1 -1
- package/dist/lib/commands.js +85 -0
- package/package.json +14 -14
- package/dist/components/chat/markdown.d.ts +0 -9
- package/dist/components/chat/markdown.d.ts.map +0 -1
- 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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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: () =>
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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 (
|
|
124
|
+
return (_jsx(FileOperation, { operation: "write", path: path || 'file', status: "loading" }));
|
|
112
125
|
},
|
|
113
126
|
renderOutput: (output, toolCallId) => {
|
|
114
127
|
const result = (output ?? {});
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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',
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
256
|
+
return (_jsx(FileOperation, { operation: "delete", path: path || 'file', status: "loading" }));
|
|
195
257
|
},
|
|
196
258
|
renderOutput: (output, toolCallId) => {
|
|
197
259
|
const result = (output ?? {});
|
|
198
|
-
|
|
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 (
|
|
273
|
+
return (_jsx(FileOperation, { operation: "edit", path: path || 'file', status: "loading" }));
|
|
208
274
|
},
|
|
209
275
|
renderOutput: (output, toolCallId) => {
|
|
210
276
|
const result = (output ?? {});
|
|
211
|
-
|
|
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: `
|
|
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?: "
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -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"}
|
package/dist/lib/commands.js
CHANGED
|
@@ -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
|
+
}
|