@moldable-ai/ui 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-message.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chat-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-message.tsx"],"names":[],"mappings":"AAgCA,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAA;QACV,QAAQ,CAAC,EAAE,OAAO,CAAA;KACnB,CAAA;CACF,CAAA;AAEL,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAA;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B,CAAA;AAED,KAAK,YAAY,GAAG;IAClB,OAAO,EAAE,WAAW,CAAA;IACpB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAcD,iBAAS,WAAW,CAAC,EACnB,OAAO,EACP,MAAc,EACd,WAAmB,GACpB,EAAE,YAAY,2CA8bd;AAED,eAAO,MAAM,OAAO,yDAAoB,CAAA;AAExC,wBAAgB,eAAe,4CAiB9B"}
|
|
@@ -6,10 +6,19 @@ import { cn } from '../../lib/utils';
|
|
|
6
6
|
import { Markdown } from '../markdown';
|
|
7
7
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '../ui/collapsible';
|
|
8
8
|
import { ThinkingTimeline, ThinkingTimelineMarker, } from './thinking-timeline';
|
|
9
|
+
import { ToolApproval, ToolApprovalHeader, ToolApprovalRejected, } from './tool-approval';
|
|
10
|
+
import { useToolApprovalResponse } from './tool-approval-context';
|
|
9
11
|
import { getToolHandler } from './tool-handlers';
|
|
12
|
+
import { useToolProgress } from './tool-progress-context';
|
|
10
13
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
11
14
|
const DEFAULT_ACTIONS_LABEL = 'Thinking';
|
|
15
|
+
// OpenRouter sends these for encrypted reasoning msgs
|
|
16
|
+
const stripRedacted = (text) => text.replace(/\[REDACTED\]/g, '').trim();
|
|
12
17
|
function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
18
|
+
// Get tool progress for streaming stdout/stderr
|
|
19
|
+
const toolProgress = useToolProgress();
|
|
20
|
+
// Get approval response handler
|
|
21
|
+
const onApprovalResponse = useToolApprovalResponse();
|
|
13
22
|
// Track open state for each thinking group independently
|
|
14
23
|
const [thinkingGroupStates, setThinkingGroupStates] = useState(new Map());
|
|
15
24
|
// Process parts chronologically, grouping only reasoning into thinking groups
|
|
@@ -69,13 +78,51 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
69
78
|
if (toolHandler.inline) {
|
|
70
79
|
// Close any open thinking group first
|
|
71
80
|
closeThinkingGroup();
|
|
81
|
+
// Check for approval states
|
|
82
|
+
const isApprovalRequested = part.state === 'approval-requested';
|
|
83
|
+
const isApprovalResponded = part.state === 'approval-responded' ||
|
|
84
|
+
part.state === 'output-denied';
|
|
85
|
+
const wasRejected = part.approval?.approved === false;
|
|
86
|
+
const wasApproved = part.approval?.approved === true;
|
|
72
87
|
// Render inline tool
|
|
88
|
+
// Also treat approval-responded (when approved) as loading until output arrives
|
|
73
89
|
const isToolLoading = part.state === 'partial-call' ||
|
|
74
90
|
part.state === 'call' ||
|
|
75
|
-
part.state === 'pending'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
part.state === 'pending' ||
|
|
92
|
+
(isApprovalResponded && wasApproved && part.output === undefined);
|
|
93
|
+
// Check for streaming progress (stdout/stderr from command execution)
|
|
94
|
+
const progress = part.toolCallId
|
|
95
|
+
? toolProgress[part.toolCallId]
|
|
96
|
+
: undefined;
|
|
97
|
+
const hasStreamingOutput = progress && (progress.stdout || progress.stderr);
|
|
98
|
+
let toolContent;
|
|
99
|
+
// Handle approval request state
|
|
100
|
+
if (isApprovalRequested &&
|
|
101
|
+
toolHandler.renderApproval &&
|
|
102
|
+
part.approval?.id &&
|
|
103
|
+
onApprovalResponse) {
|
|
104
|
+
toolContent = toolHandler.renderApproval({
|
|
105
|
+
approvalId: part.approval.id,
|
|
106
|
+
toolCallId: part.toolCallId || key,
|
|
107
|
+
args: part.args,
|
|
108
|
+
}, onApprovalResponse);
|
|
109
|
+
}
|
|
110
|
+
else if (isApprovalResponded && wasRejected) {
|
|
111
|
+
// Show rejection message
|
|
112
|
+
toolContent = (_jsx(ToolApproval, { state: "output-denied", approved: false, children: _jsx(ToolApprovalHeader, { children: _jsx(ToolApprovalRejected, { children: "Command execution was rejected" }) }) }));
|
|
113
|
+
}
|
|
114
|
+
else if (hasStreamingOutput && toolHandler.renderStreaming) {
|
|
115
|
+
// Show streaming output (live stdout/stderr)
|
|
116
|
+
toolContent = toolHandler.renderStreaming(part.args, progress);
|
|
117
|
+
}
|
|
118
|
+
else if (isToolLoading) {
|
|
119
|
+
// Show loading state (arguments streaming or waiting)
|
|
120
|
+
toolContent = toolHandler.renderLoading?.(part.args) || (_jsx("div", { className: "text-muted-foreground text-xs", children: toolHandler.loadingLabel }));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Show final output
|
|
124
|
+
toolContent = toolHandler.renderOutput(part.output, part.toolCallId || key);
|
|
125
|
+
}
|
|
79
126
|
items.push({
|
|
80
127
|
type: 'inline-tool',
|
|
81
128
|
id: key,
|
|
@@ -110,15 +157,15 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
110
157
|
}
|
|
111
158
|
if (part.type === 'reasoning') {
|
|
112
159
|
const full = part.text ?? '';
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
160
|
+
const cleaned = stripRedacted(full);
|
|
161
|
+
if (cleaned.length === 0) {
|
|
115
162
|
continue;
|
|
116
163
|
}
|
|
117
164
|
const group = ensureThinkingGroup();
|
|
118
165
|
// Extract title from markdown **title** or use first line
|
|
119
|
-
const markdownTitleMatch =
|
|
166
|
+
const markdownTitleMatch = cleaned.match(/\*\*([^*]+)\*\*/);
|
|
120
167
|
const candidateTitle = markdownTitleMatch?.[1];
|
|
121
|
-
const trimmed =
|
|
168
|
+
const trimmed = cleaned;
|
|
122
169
|
const firstLine = trimmed.split(/\n+/)[0];
|
|
123
170
|
const extractedTitle = candidateTitle || firstLine?.slice(0, 50) || DEFAULT_ACTIONS_LABEL;
|
|
124
171
|
group.title = extractedTitle;
|
|
@@ -148,7 +195,13 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
148
195
|
contentItems: items,
|
|
149
196
|
hasFinalAssistantText: state.hasText,
|
|
150
197
|
};
|
|
151
|
-
}, [
|
|
198
|
+
}, [
|
|
199
|
+
message.id,
|
|
200
|
+
message.parts,
|
|
201
|
+
message.content,
|
|
202
|
+
toolProgress,
|
|
203
|
+
onApprovalResponse,
|
|
204
|
+
]);
|
|
152
205
|
// Determine if last thinking group is streaming
|
|
153
206
|
const lastThinkingGroup = contentItems
|
|
154
207
|
.filter((item) => item.type === 'thinking')
|
|
@@ -236,8 +289,9 @@ function PureMessage({ message, isLast = false, isStreaming = false, }) {
|
|
|
236
289
|
return (_jsx("div", { className: "w-full min-w-0", children: item.content }, item.id));
|
|
237
290
|
}
|
|
238
291
|
if (item.type === 'text' && item.textPart) {
|
|
239
|
-
return (_jsx("div", { className: cn('flex flex-col gap-4', {
|
|
292
|
+
return (_jsx("div", { className: cn('flex min-w-0 flex-col gap-4', {
|
|
240
293
|
'bg-secondary text-secondary-foreground w-fit rounded-2xl px-4 py-2.5': message.role === 'user',
|
|
294
|
+
'w-full': message.role === 'assistant',
|
|
241
295
|
}), children: _jsx(Markdown, { markdown: item.textPart.text, proseSize: "sm", className: cn('[&_.prose]:leading-relaxed [&_.prose]:text-current', '[&_.prose_pre]:bg-background/80 [&_.prose_code]:text-current', '[&_.prose_p:last-child]:mb-0 [&_.prose_p]:mb-2', message.role === 'user' &&
|
|
242
296
|
'[&_.prose_pre]:bg-secondary-foreground/20') }) }, item.id));
|
|
243
297
|
}
|
|
@@ -3,7 +3,18 @@ import type { ChatMessage } from './chat-message';
|
|
|
3
3
|
import { type ConversationMeta } from './conversation-history';
|
|
4
4
|
import { type ModelOption } from './model-selector';
|
|
5
5
|
import { type ReasoningEffortOption } from './reasoning-effort-selector';
|
|
6
|
+
import { type ApprovalResponseHandler } from './tool-approval-context';
|
|
6
7
|
type ChatStatus = 'ready' | 'submitted' | 'streaming' | 'error';
|
|
8
|
+
/**
|
|
9
|
+
* Progress data for a running tool (command execution with streaming stdout/stderr)
|
|
10
|
+
*/
|
|
11
|
+
export interface ToolProgressData {
|
|
12
|
+
toolCallId: string;
|
|
13
|
+
command: string;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
status: 'running' | 'complete';
|
|
17
|
+
}
|
|
7
18
|
export interface ChatPanelProps {
|
|
8
19
|
/** Chat messages */
|
|
9
20
|
messages: ChatMessage[];
|
|
@@ -47,6 +58,10 @@ export interface ChatPanelProps {
|
|
|
47
58
|
isExpanded: boolean;
|
|
48
59
|
/** Toggle expanded state */
|
|
49
60
|
onExpandedChange: (expanded: boolean) => void;
|
|
61
|
+
/** Whether the panel is minimized (hidden below fold with only flap visible) */
|
|
62
|
+
isMinimized?: boolean;
|
|
63
|
+
/** Toggle minimized state */
|
|
64
|
+
onMinimizedChange?: (minimized: boolean) => void;
|
|
50
65
|
/** Custom class name */
|
|
51
66
|
className?: string;
|
|
52
67
|
/** Error from chat request */
|
|
@@ -55,10 +70,14 @@ export interface ChatPanelProps {
|
|
|
55
70
|
missingApiKey?: boolean;
|
|
56
71
|
/** Callback to trigger API key setup */
|
|
57
72
|
onAddApiKey?: () => void;
|
|
73
|
+
/** Progress data for running tools (streaming stdout/stderr) */
|
|
74
|
+
toolProgress?: Record<string, ToolProgressData>;
|
|
75
|
+
/** Callback for tool approval responses */
|
|
76
|
+
onApprovalResponse?: ApprovalResponseHandler;
|
|
58
77
|
}
|
|
59
78
|
/**
|
|
60
79
|
* Floating chat panel with model selector
|
|
61
80
|
*/
|
|
62
|
-
export declare function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder, welcomeMessage, isExpanded, onExpandedChange, className, error, missingApiKey, onAddApiKey, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
81
|
+
export declare function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder, welcomeMessage, isExpanded, onExpandedChange, isMinimized, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress, onApprovalResponse, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
63
82
|
export {};
|
|
64
83
|
//# sourceMappingURL=chat-panel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-panel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-panel.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,SAAS,EAKf,MAAM,OAAO,CAAA;AAWd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,KAAK,qBAAqB,EAE3B,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"chat-panel.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-panel.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,SAAS,EAKf,MAAM,OAAO,CAAA;AAWd,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,KAAK,WAAW,EAAiB,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,KAAK,qBAAqB,EAE3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,yBAAyB,CAAA;AAIhC,KAAK,UAAU,GAAG,OAAO,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;AAE/D;;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;AAED,MAAM,WAAW,cAAc;IAC7B,oBAAoB;IACpB,QAAQ,EAAE,WAAW,EAAE,CAAA;IACvB,0BAA0B;IAC1B,MAAM,EAAE,UAAU,CAAA;IAClB,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,0BAA0B;IAC1B,aAAa,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,mBAAmB,CAAC,KAAK,IAAI,CAAA;IAC5D,yBAAyB;IACzB,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAA;IAClD,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB,sBAAsB;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,uBAAuB;IACvB,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,wBAAwB;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,0BAA0B;IAC1B,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,kEAAkE;IAClE,sBAAsB,CAAC,EAAE,qBAAqB,EAAE,CAAA;IAChD,gCAAgC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,qCAAqC;IACrC,uBAAuB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;IAClD,2BAA2B;IAC3B,aAAa,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAClC,8BAA8B;IAC9B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,oCAAoC;IACpC,oBAAoB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,mCAAmC;IACnC,oBAAoB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAC3C,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,SAAS,CAAA;IAC1B,oCAAoC;IACpC,UAAU,EAAE,OAAO,CAAA;IACnB,4BAA4B;IAC5B,gBAAgB,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAA;IAC7C,gFAAgF;IAChF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,6BAA6B;IAC7B,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;IAChD,wBAAwB;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8BAA8B;IAC9B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,mCAAmC;IACnC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IAC/C,2CAA2C;IAC3C,kBAAkB,CAAC,EAAE,uBAAuB,CAAA;CAC7C;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,MAAM,EACN,KAAK,EACL,aAAa,EACb,QAAQ,EACR,MAAM,EACN,SAAS,EACT,MAAM,EACN,aAAa,EACb,aAAa,EACb,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,WAA+B,EAC/B,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,WAAmB,EACnB,iBAAiB,EACjB,SAAS,EACT,KAAK,EACL,aAAa,EACb,WAAW,EACX,YAAiB,EACjB,kBAAkB,GACnB,EAAE,cAAc,2CAiWhB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { AlertCircle,
|
|
3
|
+
import { AlertCircle, ChevronDown, MessageCircle, Plus } from 'lucide-react';
|
|
4
4
|
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
6
|
import { Button } from '../ui/button';
|
|
@@ -11,11 +11,13 @@ import { Messages } from './chat-messages';
|
|
|
11
11
|
import { ConversationHistory, } from './conversation-history';
|
|
12
12
|
import { ModelSelector } from './model-selector';
|
|
13
13
|
import { ReasoningEffortSelector, } from './reasoning-effort-selector';
|
|
14
|
+
import { ToolApprovalProvider, } from './tool-approval-context';
|
|
15
|
+
import { ToolProgressProvider } from './tool-progress-context';
|
|
14
16
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
15
17
|
/**
|
|
16
18
|
* Floating chat panel with model selector
|
|
17
19
|
*/
|
|
18
|
-
export function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, className, error, missingApiKey, onAddApiKey, }) {
|
|
20
|
+
export function ChatPanel({ messages, status, input, onInputChange, onSubmit, onStop, onNewChat, models, selectedModel, onModelChange, reasoningEffortOptions, selectedReasoningEffort, onReasoningEffortChange, conversations, currentConversationId, onSelectConversation, onDeleteConversation, placeholder = 'Ask anything...', welcomeMessage, isExpanded, onExpandedChange, isMinimized = false, onMinimizedChange, className, error, missingApiKey, onAddApiKey, toolProgress = {}, onApprovalResponse, }) {
|
|
19
21
|
const inputRef = useRef(null);
|
|
20
22
|
const messagesEndRef = useRef(null);
|
|
21
23
|
const scrollAreaRef = useRef(null);
|
|
@@ -87,9 +89,22 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
87
89
|
document.addEventListener('keydown', handleKeyDown);
|
|
88
90
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
89
91
|
}, [isResponding, isExpanded, handleNewChat]);
|
|
90
|
-
|
|
92
|
+
// Handle clicking the minimized flap
|
|
93
|
+
const handleFlapClick = useCallback(() => {
|
|
94
|
+
onMinimizedChange?.(false);
|
|
95
|
+
}, [onMinimizedChange]);
|
|
96
|
+
// Handle minimize button - now goes to minimized state
|
|
97
|
+
const handleMinimize = useCallback(() => {
|
|
98
|
+
if (isExpanded) {
|
|
99
|
+
onExpandedChange(false);
|
|
100
|
+
}
|
|
101
|
+
onMinimizedChange?.(true);
|
|
102
|
+
}, [isExpanded, onExpandedChange, onMinimizedChange]);
|
|
103
|
+
return (_jsxs(_Fragment, { children: [_jsx(AnimatePresence, { children: isExpanded && !isMinimized && (_jsx(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 }, className: "fixed inset-0 z-40 bg-black/20", onClick: () => onExpandedChange(false) })) }), _jsx(AnimatePresence, { children: isMinimized && (_jsx(motion.div, { initial: { y: 20, opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: 20, opacity: 0 }, transition: { type: 'tween', duration: 0.2, ease: 'easeOut' }, className: "fixed bottom-0 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 -translate-x-1/2", children: _jsxs("button", { onClick: handleFlapClick, className: cn('bg-background/95 hover:bg-background group flex cursor-pointer items-center gap-2 rounded-t-xl border border-b-0 px-4 py-2 shadow-lg backdrop-blur transition-colors', 'text-muted-foreground hover:text-foreground'), children: [_jsx(MessageCircle, { className: "size-4" }), _jsx("span", { className: "text-sm font-medium", children: "Chat" }), _jsx("span", { className: "bg-muted text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary rounded px-1.5 py-0.5 text-xs transition-colors", children: "\u2318M" })] }) })) }), _jsx(motion.div, { initial: false, animate: {
|
|
91
104
|
width: isExpanded ? 'min(90vw, 640px)' : '400px',
|
|
92
|
-
|
|
105
|
+
y: isMinimized ? 'calc(100% + 24px)' : 0,
|
|
106
|
+
opacity: isMinimized ? 0 : 1,
|
|
107
|
+
}, transition: { type: 'tween', duration: 0.25, ease: 'easeOut' }, className: cn('pointer-events-none fixed bottom-6 left-[calc(50%+var(--sidebar-width,0px)/2)] z-50 flex -translate-x-1/2 justify-center', isMinimized && 'pointer-events-none', className), children: _jsxs("div", { className: cn('bg-background pointer-events-auto relative w-full border shadow-lg', 'supports-[backdrop-filter]:bg-background/95 backdrop-blur', 'rounded-4xl', isExpanded ? 'overflow-hidden' : 'overflow-visible'), children: [!isExpanded && (_jsx(motion.div, { className: "rounded-4xl pointer-events-none absolute inset-0 z-0", animate: {
|
|
93
108
|
boxShadow: [
|
|
94
109
|
'0 0 0px 0px color-mix(in oklch, var(--primary) 0%, #ff8800 00%)',
|
|
95
110
|
'0 0 20px 0px color-mix(in oklch, var(--primary) 20%, #ff8800 30%)',
|
|
@@ -107,7 +122,7 @@ export function ChatPanel({ messages, status, input, onInputChange, onSubmit, on
|
|
|
107
122
|
selectedReasoningEffort &&
|
|
108
123
|
onReasoningEffortChange && (_jsx(ReasoningEffortSelector, { options: reasoningEffortOptions, selectedEffort: selectedReasoningEffort, onEffortChange: onReasoningEffortChange, disabled: isResponding }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [conversations &&
|
|
109
124
|
conversations.length > 0 &&
|
|
110
|
-
onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, disabled: isResponding })), _jsxs(TooltipProvider, { children: [messages.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleNewChat, disabled: isResponding, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "New chat" }) })] })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick:
|
|
125
|
+
onSelectConversation && (_jsx(ConversationHistory, { conversations: conversations, currentConversationId: currentConversationId, onSelect: onSelectConversation, onDelete: onDeleteConversation, disabled: isResponding })), _jsxs(TooltipProvider, { children: [messages.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleNewChat, disabled: isResponding, className: "text-muted-foreground hover:text-foreground", children: _jsx(Plus, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "New chat" }) })] })), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: handleMinimize, className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronDown, { className: "size-4" }) }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("p", { children: "Minimize" }) })] })] })] })] }), _jsx(ScrollArea, { ref: scrollAreaRef, className: "min-w-0 flex-1 px-2", children: _jsxs("div", { className: "min-w-0 max-w-full space-y-4 overflow-hidden py-4", children: [messages.length === 0 && welcomeMessage && (_jsx("div", { className: "bg-muted/50 text-muted-foreground mx-2 rounded-2xl p-4 text-sm", children: welcomeMessage })), _jsx(ToolProgressProvider, { value: toolProgress, children: _jsx(ToolApprovalProvider, { onApprovalResponse: onApprovalResponse ?? null, children: _jsx(Messages, { messages: messages, status: status }) }) }), (missingApiKey ||
|
|
111
126
|
(error &&
|
|
112
127
|
error.message?.includes('API_KEY not configured'))) && (_jsxs("div", { className: "border-primary/30 bg-primary/5 mx-2 flex flex-col items-center gap-3 rounded-lg border p-4 text-center", children: [_jsx("div", { className: "bg-primary/10 flex size-10 items-center justify-center rounded-full", children: _jsx(AlertCircle, { className: "text-primary size-5" }) }), _jsxs("div", { children: [_jsx("p", { className: "text-foreground font-medium", children: "API key required" }), _jsx("p", { className: "text-muted-foreground mt-1 text-sm", children: "Add an API key to start chatting with AI" })] }), onAddApiKey && (_jsx(Button, { onClick: onAddApiKey, size: "sm", className: "cursor-pointer", children: "Add API key" }))] })), error &&
|
|
113
128
|
status === 'error' &&
|
|
@@ -4,7 +4,10 @@ export { Messages } from './chat-messages';
|
|
|
4
4
|
export { ModelSelector, type ModelOption } from './model-selector';
|
|
5
5
|
export { ReasoningEffortSelector, type ReasoningEffortOption, } from './reasoning-effort-selector';
|
|
6
6
|
export { ConversationHistory, type ConversationMeta, } from './conversation-history';
|
|
7
|
-
export { ChatPanel, type ChatPanelProps } from './chat-panel';
|
|
7
|
+
export { ChatPanel, type ChatPanelProps, type ToolProgressData, } from './chat-panel';
|
|
8
8
|
export { ThinkingTimeline, ThinkingTimelineMarker, type ThinkingTimelineItem, type ThinkingTimelineProps, } from './thinking-timeline';
|
|
9
9
|
export { getToolHandler, DEFAULT_TOOL_HANDLERS, type ToolHandler, } from './tool-handlers';
|
|
10
|
+
export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
|
|
11
|
+
export { ToolApprovalProvider, useToolApprovalResponse, type ApprovalResponseHandler, } from './tool-approval-context';
|
|
12
|
+
export { ToolApproval, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalAccepted, ToolApprovalRejected, ToolApprovalActions, ToolApprovalAction, type ToolApprovalState, } from './tool-approval';
|
|
10
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/chat/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EACL,OAAO,EACP,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EACL,uBAAuB,EACvB,KAAK,qBAAqB,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,SAAS,EACT,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,mBAAmB,GACpB,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,uBAAuB,GAC7B,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAA"}
|
|
@@ -4,6 +4,9 @@ export { Messages } from './chat-messages';
|
|
|
4
4
|
export { ModelSelector } from './model-selector';
|
|
5
5
|
export { ReasoningEffortSelector, } from './reasoning-effort-selector';
|
|
6
6
|
export { ConversationHistory, } from './conversation-history';
|
|
7
|
-
export { ChatPanel } from './chat-panel';
|
|
7
|
+
export { ChatPanel, } from './chat-panel';
|
|
8
8
|
export { ThinkingTimeline, ThinkingTimelineMarker, } from './thinking-timeline';
|
|
9
9
|
export { getToolHandler, DEFAULT_TOOL_HANDLERS, } from './tool-handlers';
|
|
10
|
+
export { ToolProgressProvider, useToolProgress, useToolCallProgress, } from './tool-progress-context';
|
|
11
|
+
export { ToolApprovalProvider, useToolApprovalResponse, } from './tool-approval-context';
|
|
12
|
+
export { ToolApproval, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalAccepted, ToolApprovalRejected, ToolApprovalActions, ToolApprovalAction, } from './tool-approval';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Callback type for responding to tool approval requests
|
|
4
|
+
*/
|
|
5
|
+
export type ApprovalResponseHandler = (params: {
|
|
6
|
+
approvalId: string;
|
|
7
|
+
approved: boolean;
|
|
8
|
+
reason?: string;
|
|
9
|
+
}) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Provider for tool approval handling
|
|
12
|
+
*/
|
|
13
|
+
export declare function ToolApprovalProvider({ onApprovalResponse, children, }: {
|
|
14
|
+
onApprovalResponse: ApprovalResponseHandler | null;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Hook to get the approval response handler
|
|
19
|
+
*/
|
|
20
|
+
export declare function useToolApprovalResponse(): ApprovalResponseHandler | null;
|
|
21
|
+
//# sourceMappingURL=tool-approval-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-approval-context.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-approval-context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAA6B,MAAM,OAAO,CAAA;AAEjE;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,MAAM,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,KAAK,IAAI,CAAA;AAOV;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,kBAAkB,EAClB,QAAQ,GACT,EAAE;IACD,kBAAkB,EAAE,uBAAuB,GAAG,IAAI,CAAA;IAClD,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAMA;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,uBAAuB,GAAG,IAAI,CAExE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Context for tool approval handling
|
|
6
|
+
*/
|
|
7
|
+
const ToolApprovalContext = createContext(null);
|
|
8
|
+
/**
|
|
9
|
+
* Provider for tool approval handling
|
|
10
|
+
*/
|
|
11
|
+
export function ToolApprovalProvider({ onApprovalResponse, children, }) {
|
|
12
|
+
return (_jsx(ToolApprovalContext.Provider, { value: onApprovalResponse, children: children }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook to get the approval response handler
|
|
16
|
+
*/
|
|
17
|
+
export function useToolApprovalResponse() {
|
|
18
|
+
return useContext(ToolApprovalContext);
|
|
19
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Button } from '../ui/button';
|
|
3
|
+
/**
|
|
4
|
+
* State of the tool approval
|
|
5
|
+
*/
|
|
6
|
+
export type ToolApprovalState = 'approval-requested' | 'approval-responded' | 'output-available' | 'output-denied';
|
|
7
|
+
/**
|
|
8
|
+
* Root component for tool approval UI
|
|
9
|
+
*/
|
|
10
|
+
interface ToolApprovalProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/** Current state of the approval */
|
|
13
|
+
state: ToolApprovalState;
|
|
14
|
+
/** Whether the user approved (for responded/output states) */
|
|
15
|
+
approved?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function ToolApproval({ children, state, approved, className, }: ToolApprovalProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Header section with icon and title
|
|
21
|
+
*/
|
|
22
|
+
interface ToolApprovalHeaderProps {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function ToolApprovalHeader({ children, className, }: ToolApprovalHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
/**
|
|
28
|
+
* Content shown only when approval is being requested
|
|
29
|
+
*/
|
|
30
|
+
interface ToolApprovalRequestProps {
|
|
31
|
+
children: React.ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function ToolApprovalRequest({ children, className, }: ToolApprovalRequestProps): import("react/jsx-runtime").JSX.Element | null;
|
|
35
|
+
/**
|
|
36
|
+
* Expandable help section explaining why approval is needed
|
|
37
|
+
*/
|
|
38
|
+
interface ToolApprovalHelpProps {
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function ToolApprovalHelp({ children, className, }: ToolApprovalHelpProps): import("react/jsx-runtime").JSX.Element | null;
|
|
43
|
+
/**
|
|
44
|
+
* Pre-built help content for sandboxing explanation
|
|
45
|
+
*/
|
|
46
|
+
export declare function ToolApprovalSandboxHelp({ className }: {
|
|
47
|
+
className?: string;
|
|
48
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
/**
|
|
50
|
+
* Pre-built help content for dangerous command explanation
|
|
51
|
+
*/
|
|
52
|
+
export declare function ToolApprovalDangerousHelp({ className, }: {
|
|
53
|
+
className?: string;
|
|
54
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
55
|
+
/**
|
|
56
|
+
* Content shown when approval was granted
|
|
57
|
+
*/
|
|
58
|
+
interface ToolApprovalAcceptedProps {
|
|
59
|
+
children?: React.ReactNode;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function ToolApprovalAccepted({ children, className, }: ToolApprovalAcceptedProps): import("react/jsx-runtime").JSX.Element | null;
|
|
63
|
+
/**
|
|
64
|
+
* Content shown when approval was rejected
|
|
65
|
+
*/
|
|
66
|
+
interface ToolApprovalRejectedProps {
|
|
67
|
+
children?: React.ReactNode;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
export declare function ToolApprovalRejected({ children, className, }: ToolApprovalRejectedProps): import("react/jsx-runtime").JSX.Element | null;
|
|
71
|
+
/**
|
|
72
|
+
* Actions container (approve/reject buttons)
|
|
73
|
+
*/
|
|
74
|
+
interface ToolApprovalActionsProps {
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
className?: string;
|
|
77
|
+
}
|
|
78
|
+
export declare function ToolApprovalActions({ children, className, }: ToolApprovalActionsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
79
|
+
/**
|
|
80
|
+
* Individual action button
|
|
81
|
+
*/
|
|
82
|
+
type ToolApprovalActionProps = React.ComponentProps<typeof Button>;
|
|
83
|
+
export declare function ToolApprovalAction({ className, size, ...props }: ToolApprovalActionProps): import("react/jsx-runtime").JSX.Element;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=tool-approval.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-approval.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-approval.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB,oBAAoB,GACpB,oBAAoB,GACpB,kBAAkB,GAClB,eAAe,CAAA;AAenB;;GAEG;AACH,UAAU,iBAAiB;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,oCAAoC;IACpC,KAAK,EAAE,iBAAiB,CAAA;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,SAAS,GACV,EAAE,iBAAiB,2CAanB;AAED;;GAEG;AACH,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,SAAS,GACV,EAAE,uBAAuB,2CAqBzB;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,SAAS,GACV,EAAE,wBAAwB,kDAQ1B;AAED;;GAEG;AACH,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,SAAS,GACV,EAAE,qBAAqB,kDAkCvB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAc5E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,SAAS,GACV,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,2CAaA;AAED;;GAEG;AACH,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,SAAS,GACV,EAAE,yBAAyB,kDAqB3B;AAED;;GAEG;AACH,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,SAAS,GACV,EAAE,yBAAyB,kDAuB3B;AAED;;GAEG;AACH,UAAU,wBAAwB;IAChC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,SAAS,GACV,EAAE,wBAAwB,kDAkB1B;AAED;;GAEG;AACH,KAAK,uBAAuB,GAAG,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,CAAA;AAElE,wBAAgB,kBAAkB,CAAC,EACjC,SAAS,EACT,IAAW,EACX,GAAG,KAAK,EACT,EAAE,uBAAuB,2CAIzB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Check, ChevronDown, HelpCircle, ShieldAlert, X } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
const ToolApprovalContext = React.createContext({
|
|
8
|
+
state: 'approval-requested',
|
|
9
|
+
});
|
|
10
|
+
function useToolApprovalContext() {
|
|
11
|
+
return React.useContext(ToolApprovalContext);
|
|
12
|
+
}
|
|
13
|
+
export function ToolApproval({ children, state, approved, className, }) {
|
|
14
|
+
return (_jsx(ToolApprovalContext.Provider, { value: { state, approved }, children: _jsx("div", { className: cn('bg-card border-border my-2 overflow-hidden rounded-lg border', className), children: children }) }));
|
|
15
|
+
}
|
|
16
|
+
export function ToolApprovalHeader({ children, className, }) {
|
|
17
|
+
const { state, approved } = useToolApprovalContext();
|
|
18
|
+
// Show different icons based on state
|
|
19
|
+
const icon = state === 'approval-requested' ? (_jsx(ShieldAlert, { className: "size-3.5 shrink-0 text-amber-500" })) : approved ? (_jsx(Check, { className: "text-success size-3.5 shrink-0" })) : (_jsx(X, { className: "text-destructive size-3.5 shrink-0" }));
|
|
20
|
+
return (_jsxs("div", { className: cn('bg-muted/50 flex items-start gap-2 px-3 py-2', className), children: [_jsx("div", { className: "mt-0.5", children: icon }), _jsx("div", { className: "min-w-0 flex-1", children: children })] }));
|
|
21
|
+
}
|
|
22
|
+
export function ToolApprovalRequest({ children, className, }) {
|
|
23
|
+
const { state } = useToolApprovalContext();
|
|
24
|
+
if (state !== 'approval-requested') {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return _jsx("div", { className: cn('text-xs', className), children: children });
|
|
28
|
+
}
|
|
29
|
+
export function ToolApprovalHelp({ children, className, }) {
|
|
30
|
+
const { state } = useToolApprovalContext();
|
|
31
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
32
|
+
// Only show when approval is being requested
|
|
33
|
+
if (state !== 'approval-requested') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return (_jsxs("div", { className: cn('', className), children: [_jsxs("button", { type: "button", onClick: () => setIsOpen(!isOpen), className: "text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1 text-[10px] transition-colors", children: [_jsx(HelpCircle, { className: "size-2.5" }), _jsx("span", { children: "What does this mean?" }), _jsx(ChevronDown, { className: cn('size-2.5 transition-transform', isOpen && 'rotate-180') })] }), isOpen && (_jsx("div", { className: "bg-muted/30 mt-1.5 rounded-md px-2 py-1.5", children: _jsx("p", { className: "text-muted-foreground text-[10px] leading-relaxed", children: children }) }))] }));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pre-built help content for sandboxing explanation
|
|
40
|
+
*/
|
|
41
|
+
export function ToolApprovalSandboxHelp({ className }) {
|
|
42
|
+
return (_jsxs(ToolApprovalHelp, { className: className, children: [_jsx("strong", { className: "text-foreground", children: "Sandboxing" }), " restricts commands from accessing the network and writing to sensitive locations. Package managers like", ' ', _jsx("code", { className: "bg-muted rounded px-1 py-0.5", children: "pnpm install" }), " need network access to download packages, so they run unsandboxed.", _jsxs("strong", { className: "text-foreground", children: [' ', "Only approve commands you trust."] })] }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pre-built help content for dangerous command explanation
|
|
46
|
+
*/
|
|
47
|
+
export function ToolApprovalDangerousHelp({ className, }) {
|
|
48
|
+
return (_jsxs(ToolApprovalHelp, { className: className, children: [_jsx("strong", { className: "text-foreground", children: "Dangerous commands" }), " can make irreversible changes to your system, like deleting files recursively or modifying permissions. These commands are flagged based on common patterns (rm -rf, sudo, etc.) to give you a chance to review before execution.", _jsxs("strong", { className: "text-foreground", children: [' ', "Only approve if you understand what the command does."] })] }));
|
|
49
|
+
}
|
|
50
|
+
export function ToolApprovalAccepted({ children, className, }) {
|
|
51
|
+
const { state, approved } = useToolApprovalContext();
|
|
52
|
+
// Show for responded/output states where approved is true
|
|
53
|
+
const shouldShow = (state === 'approval-responded' || state === 'output-available') && approved;
|
|
54
|
+
if (!shouldShow) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return (_jsx("div", { className: cn('text-success flex items-center gap-1.5 text-xs', className), children: children ?? 'Approved' }));
|
|
58
|
+
}
|
|
59
|
+
export function ToolApprovalRejected({ children, className, }) {
|
|
60
|
+
const { state, approved } = useToolApprovalContext();
|
|
61
|
+
// Show for output-denied or responded with approved=false
|
|
62
|
+
const shouldShow = state === 'output-denied' ||
|
|
63
|
+
((state === 'approval-responded' || state === 'output-available') &&
|
|
64
|
+
approved === false);
|
|
65
|
+
if (!shouldShow) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("div", { className: cn('text-destructive flex items-center gap-1.5 text-xs', className), children: children ?? 'Rejected' }));
|
|
69
|
+
}
|
|
70
|
+
export function ToolApprovalActions({ children, className, }) {
|
|
71
|
+
const { state } = useToolApprovalContext();
|
|
72
|
+
// Only show actions when approval is being requested
|
|
73
|
+
if (state !== 'approval-requested') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return (_jsx("div", { className: cn('border-border flex items-center justify-end gap-2 border-t px-3 py-2', className), children: children }));
|
|
77
|
+
}
|
|
78
|
+
export function ToolApprovalAction({ className, size = 'sm', ...props }) {
|
|
79
|
+
return (_jsx(Button, { className: cn('h-7 text-xs', className), size: size, ...props }));
|
|
80
|
+
}
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { ThinkingTimelineMarker } from './thinking-timeline';
|
|
3
|
+
import { type ApprovalResponseHandler } from './tool-approval-context';
|
|
4
|
+
/**
|
|
5
|
+
* Progress data for streaming tool execution (e.g., command stdout/stderr)
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolStreamingProgress {
|
|
8
|
+
toolCallId: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
status: 'running' | 'complete';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Approval info passed to renderApproval
|
|
16
|
+
*/
|
|
17
|
+
export interface ToolApprovalInfo {
|
|
18
|
+
approvalId: string;
|
|
19
|
+
toolCallId: string;
|
|
20
|
+
args: unknown;
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* Tool handler definition
|
|
5
24
|
*/
|
|
@@ -9,6 +28,8 @@ export type ToolHandler = {
|
|
|
9
28
|
inline?: boolean;
|
|
10
29
|
renderOutput: (output: unknown, toolCallId: string) => ReactNode;
|
|
11
30
|
renderLoading?: (args?: unknown) => ReactNode;
|
|
31
|
+
renderStreaming?: (args: unknown, progress: ToolStreamingProgress) => ReactNode;
|
|
32
|
+
renderApproval?: (approval: ToolApprovalInfo, onRespond: ApprovalResponseHandler) => ReactNode;
|
|
12
33
|
};
|
|
13
34
|
/**
|
|
14
35
|
* Default tool handlers for Moldable tools
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-handlers.d.ts","sourceRoot":"","sources":["../../../src/components/chat/tool-handlers.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAA;AAEhD,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AAU5D,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AAEtE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,UAAU,CAAA;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG;IAExB,YAAY,EAAE,MAAM,CAAA;IAEpB,MAAM,CAAC,EAAE,sBAAsB,CAAA;IAE/B,MAAM,CAAC,EAAE,OAAO,CAAA;IAEhB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,SAAS,CAAA;IAGhE,aAAa,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,KAAK,SAAS,CAAA;IAG7C,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,qBAAqB,KAC5B,SAAS,CAAA;IAGd,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,uBAAuB,KAC/B,SAAS,CAAA;CACf,CAAA;AAkOD;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAk1C7D,CAAA;AAoBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,CAmC5D"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { BookOpen, Check, CheckCheck, Copy, Download, FileCode, FileText, FolderOpen, Globe, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
|
|
3
|
+
import { BookOpen, Check, CheckCheck, Copy, Download, FileCode, FileText, FolderOpen, Globe, Package, Plus, Search, Sparkles, Terminal, Trash2, X, } from 'lucide-react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
5
|
import { cn } from '../../lib/utils';
|
|
6
6
|
import { ThinkingTimelineMarker } from './thinking-timeline';
|
|
7
|
+
import { ToolApproval, ToolApprovalAction, ToolApprovalActions, ToolApprovalDangerousHelp, ToolApprovalHeader, ToolApprovalRequest, ToolApprovalSandboxHelp, } from './tool-approval';
|
|
7
8
|
/**
|
|
8
9
|
* Code block component for displaying command output or file contents
|
|
9
10
|
*/
|
|
@@ -30,22 +31,22 @@ function summarizeCommand(command) {
|
|
|
30
31
|
const firstLine = command.split('\n')[0];
|
|
31
32
|
// Truncate if too long
|
|
32
33
|
if (firstLine.length > 60) {
|
|
33
|
-
return firstLine.slice(0,
|
|
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
|
+
}
|
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,CA+DxE"}
|
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
|
+
}
|