@chaaskit/client 0.1.0
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/favicon.svg +11 -0
- package/dist/index.html +17 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
- package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
- package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
- package/dist/lib/extensions.js +10 -0
- package/dist/lib/extensions.js.map +1 -0
- package/dist/lib/favicon.svg +11 -0
- package/dist/lib/index.js +74126 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logo.svg +12 -0
- package/dist/lib/routes/AcceptInviteRoute.js +19 -0
- package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
- package/dist/lib/routes/AdminDashboardRoute.js +19 -0
- package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamRoute.js +19 -0
- package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
- package/dist/lib/routes/AdminTeamsRoute.js +19 -0
- package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
- package/dist/lib/routes/AdminUsersRoute.js +19 -0
- package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
- package/dist/lib/routes/ApiKeysRoute.js +19 -0
- package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
- package/dist/lib/routes/AutomationsRoute.js +19 -0
- package/dist/lib/routes/AutomationsRoute.js.map +1 -0
- package/dist/lib/routes/ChatRoute.js +19 -0
- package/dist/lib/routes/ChatRoute.js.map +1 -0
- package/dist/lib/routes/DocumentsRoute.js +19 -0
- package/dist/lib/routes/DocumentsRoute.js.map +1 -0
- package/dist/lib/routes/OAuthConsentRoute.js +19 -0
- package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
- package/dist/lib/routes/PricingRoute.js +19 -0
- package/dist/lib/routes/PricingRoute.js.map +1 -0
- package/dist/lib/routes/PrivacyRoute.js +19 -0
- package/dist/lib/routes/PrivacyRoute.js.map +1 -0
- package/dist/lib/routes/TeamSettingsRoute.js +19 -0
- package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
- package/dist/lib/routes/TermsRoute.js +19 -0
- package/dist/lib/routes/TermsRoute.js.map +1 -0
- package/dist/lib/routes/VerifyEmailRoute.js +19 -0
- package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
- package/dist/lib/routes.js +79 -0
- package/dist/lib/routes.js.map +1 -0
- package/dist/lib/ssr-utils.js +29 -0
- package/dist/lib/ssr-utils.js.map +1 -0
- package/dist/lib/ssr.js +60 -0
- package/dist/lib/ssr.js.map +1 -0
- package/dist/lib/styles.css +2410 -0
- package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
- package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
- package/dist/logo.svg +12 -0
- package/package.json +84 -0
- package/src/components/AgentSelector.tsx +90 -0
- package/src/components/BranchModal.tsx +129 -0
- package/src/components/ClientOnly.tsx +27 -0
- package/src/components/ExportMenu.tsx +122 -0
- package/src/components/LoadingSkeletons.tsx +110 -0
- package/src/components/MCPCredentialsSection.tsx +309 -0
- package/src/components/MentionChip.tsx +149 -0
- package/src/components/MentionDropdown.tsx +175 -0
- package/src/components/MentionInput.tsx +293 -0
- package/src/components/MessageItem.tsx +300 -0
- package/src/components/MessageList.tsx +159 -0
- package/src/components/OAuthAppsSection.tsx +124 -0
- package/src/components/ProjectFolder.tsx +141 -0
- package/src/components/ProjectModal.tsx +296 -0
- package/src/components/SSRMessageList.tsx +153 -0
- package/src/components/SearchModal.tsx +173 -0
- package/src/components/SettingsModal.tsx +412 -0
- package/src/components/ShareModal.tsx +280 -0
- package/src/components/Sidebar.tsx +491 -0
- package/src/components/TeamSwitcher.tsx +273 -0
- package/src/components/ToolCallDisplay.tsx +473 -0
- package/src/components/ToolConfirmationModal.tsx +130 -0
- package/src/components/UsageChart.tsx +177 -0
- package/src/components/content/CodeBlock.tsx +69 -0
- package/src/components/content/MarkdownRenderer.tsx +64 -0
- package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
- package/src/contexts/AuthContext.tsx +119 -0
- package/src/contexts/ConfigContext.tsx +214 -0
- package/src/contexts/ProjectContext.tsx +167 -0
- package/src/contexts/ServerConfigProvider.tsx +41 -0
- package/src/contexts/ServerThemeProvider.tsx +47 -0
- package/src/contexts/TeamContext.tsx +255 -0
- package/src/contexts/ThemeContext.tsx +113 -0
- package/src/extensions/index.ts +15 -0
- package/src/extensions/registry.ts +187 -0
- package/src/extensions/useExtensions.ts +52 -0
- package/src/hooks/useAppPath.ts +34 -0
- package/src/hooks/useBasePath.ts +13 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -0
- package/src/hooks/useMentionSearch.ts +106 -0
- package/src/index.tsx +116 -0
- package/src/layouts/MainLayout.tsx +98 -0
- package/src/pages/AcceptInvitePage.tsx +175 -0
- package/src/pages/AdminDashboardPage.tsx +362 -0
- package/src/pages/AdminTeamPage.tsx +304 -0
- package/src/pages/AdminTeamsPage.tsx +242 -0
- package/src/pages/AdminUsersPage.tsx +385 -0
- package/src/pages/ApiKeysPage.tsx +449 -0
- package/src/pages/ChatPage.tsx +310 -0
- package/src/pages/DocumentsPage.tsx +577 -0
- package/src/pages/LoginPage.tsx +232 -0
- package/src/pages/OAuthConsentPage.tsx +234 -0
- package/src/pages/PricingPage.tsx +314 -0
- package/src/pages/PrivacyPage.tsx +65 -0
- package/src/pages/RegisterPage.tsx +153 -0
- package/src/pages/ScheduledPromptsPage.tsx +702 -0
- package/src/pages/SharedThreadPage.tsx +116 -0
- package/src/pages/TeamSettingsPage.tsx +1085 -0
- package/src/pages/TermsPage.tsx +82 -0
- package/src/pages/VerifyEmailPage.tsx +202 -0
- package/src/routes/AcceptInviteRoute.tsx +24 -0
- package/src/routes/AdminDashboardRoute.tsx +24 -0
- package/src/routes/AdminTeamRoute.tsx +24 -0
- package/src/routes/AdminTeamsRoute.tsx +24 -0
- package/src/routes/AdminUsersRoute.tsx +24 -0
- package/src/routes/ApiKeysRoute.tsx +24 -0
- package/src/routes/AutomationsRoute.tsx +24 -0
- package/src/routes/ChatRoute.tsx +28 -0
- package/src/routes/DocumentsRoute.tsx +24 -0
- package/src/routes/OAuthConsentRoute.tsx +24 -0
- package/src/routes/PricingRoute.tsx +24 -0
- package/src/routes/PrivacyRoute.tsx +24 -0
- package/src/routes/TeamSettingsRoute.tsx +24 -0
- package/src/routes/TermsRoute.tsx +24 -0
- package/src/routes/VerifyEmailRoute.tsx +24 -0
- package/src/routes/index.ts +57 -0
- package/src/ssr-utils.tsx +84 -0
- package/src/ssr.ts +123 -0
- package/src/stores/chatStore.ts +670 -0
- package/src/styles/index.css +254 -0
- package/src/utils/api.ts +78 -0
- package/src/vite-env.d.ts +13 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { Wrench, CheckCircle, XCircle, Loader2, ChevronDown, ChevronRight, ShieldCheck } from 'lucide-react';
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
3
|
+
import type { ToolCall, ToolResult, MCPContent, UIResource, AutoApproveReason } from '@chaaskit/shared';
|
|
4
|
+
|
|
5
|
+
interface ToolCallDisplayProps {
|
|
6
|
+
toolCall: ToolCall;
|
|
7
|
+
toolResult?: ToolResult;
|
|
8
|
+
isPending?: boolean;
|
|
9
|
+
uiResource?: UIResource;
|
|
10
|
+
hideUiResource?: boolean;
|
|
11
|
+
autoApproveReason?: AutoApproveReason;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Human-readable descriptions for auto-approve reasons
|
|
15
|
+
const AUTO_APPROVE_LABELS: Record<AutoApproveReason, string> = {
|
|
16
|
+
config_none: 'Admin config allows all tools',
|
|
17
|
+
whitelist: 'Tool is in allowed list',
|
|
18
|
+
user_always: 'You always allowed this tool',
|
|
19
|
+
thread_allowed: 'Allowed for this chat',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Generate the window.openai initialization script for OpenAI format resources
|
|
23
|
+
function generateOpenAiScript(
|
|
24
|
+
toolInput: Record<string, unknown>,
|
|
25
|
+
toolOutput: MCPContent[] | Record<string, unknown>,
|
|
26
|
+
theme: string
|
|
27
|
+
): string {
|
|
28
|
+
// Map app theme to OpenAI theme (light/dark) and colors
|
|
29
|
+
const openAiTheme = theme === 'dark' ? 'dark' : 'light';
|
|
30
|
+
const backgroundColor = theme === 'dark' ? '#111827' : '#ffffff';
|
|
31
|
+
const scrollbarTrack = theme === 'dark' ? '#1f2937' : '#f3f4f6';
|
|
32
|
+
const scrollbarThumb = theme === 'dark' ? '#4b5563' : '#d1d5db';
|
|
33
|
+
const scrollbarThumbHover = theme === 'dark' ? '#6b7280' : '#9ca3af';
|
|
34
|
+
|
|
35
|
+
return `
|
|
36
|
+
<style>
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0;
|
|
40
|
+
background-color: ${backgroundColor};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Scrollbar styling for WebKit browsers (Chrome, Safari, Edge) */
|
|
44
|
+
::-webkit-scrollbar {
|
|
45
|
+
width: 8px;
|
|
46
|
+
height: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
::-webkit-scrollbar-track {
|
|
50
|
+
background: ${scrollbarTrack};
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
::-webkit-scrollbar-thumb {
|
|
55
|
+
background: ${scrollbarThumb};
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
::-webkit-scrollbar-thumb:hover {
|
|
60
|
+
background: ${scrollbarThumbHover};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Scrollbar styling for Firefox */
|
|
64
|
+
* {
|
|
65
|
+
scrollbar-width: thin;
|
|
66
|
+
scrollbar-color: ${scrollbarThumb} ${scrollbarTrack};
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
69
|
+
<script>
|
|
70
|
+
(function() {
|
|
71
|
+
// Initialize window.openai with the OpenAI Apps SDK spec
|
|
72
|
+
window.openai = {
|
|
73
|
+
// Core globals
|
|
74
|
+
theme: '${openAiTheme}',
|
|
75
|
+
userAgent: {
|
|
76
|
+
device: { type: 'desktop' },
|
|
77
|
+
capabilities: { hover: true, touch: false }
|
|
78
|
+
},
|
|
79
|
+
locale: navigator.language || 'en-US',
|
|
80
|
+
maxHeight: 800,
|
|
81
|
+
displayMode: 'inline',
|
|
82
|
+
safeArea: {
|
|
83
|
+
insets: { top: 0, bottom: 0, left: 0, right: 0 }
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Tool data
|
|
87
|
+
toolOutput: ${JSON.stringify(toolOutput)},
|
|
88
|
+
toolInput: ${JSON.stringify(toolInput)},
|
|
89
|
+
toolResponseMetadata: null,
|
|
90
|
+
widgetState: null,
|
|
91
|
+
|
|
92
|
+
// API methods
|
|
93
|
+
callTool: async (name, args) => {
|
|
94
|
+
console.log('window.openai.callTool called:', { name, args });
|
|
95
|
+
// TODO: Implement actual tool calling via parent window messaging
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: 'Tool calling not yet implemented' }],
|
|
98
|
+
isError: false
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
sendFollowUpMessage: async (args) => {
|
|
103
|
+
console.log('window.openai.sendFollowUpMessage called:', args);
|
|
104
|
+
// TODO: Implement via parent window messaging
|
|
105
|
+
return {};
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
openExternal: (payload) => {
|
|
109
|
+
console.log('window.openai.openExternal called:', payload);
|
|
110
|
+
if (payload && payload.href) {
|
|
111
|
+
window.open(payload.href, '_blank');
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
requestDisplayMode: async (args) => {
|
|
116
|
+
console.log('window.openai.requestDisplayMode called:', args);
|
|
117
|
+
return { mode: args.mode };
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
setWidgetState: async (state) => {
|
|
121
|
+
console.log('window.openai.setWidgetState called:', state);
|
|
122
|
+
window.openai.widgetState = state;
|
|
123
|
+
return {};
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
requestClose: () => {
|
|
127
|
+
console.log('window.openai.requestClose called');
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
getFileDownloadUrl: async ({ fileId }) => {
|
|
131
|
+
console.log('window.openai.getFileDownloadUrl called:', fileId);
|
|
132
|
+
return { url: '' };
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
uploadFile: async (file) => {
|
|
136
|
+
console.log('window.openai.uploadFile called:', file);
|
|
137
|
+
return { fileId: '' };
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
console.log('window.openai initialized', window.openai);
|
|
142
|
+
})();
|
|
143
|
+
</script>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Separate component for rendering UI resource widgets
|
|
148
|
+
export function UIResourceWidget({ uiResource, theme }: { uiResource: UIResource; theme: string }) {
|
|
149
|
+
const [showRawResult, setShowRawResult] = useState(false);
|
|
150
|
+
|
|
151
|
+
// Prepare iframe content with window.openai injection for OpenAI format
|
|
152
|
+
const iframeSrcDoc = useMemo(() => {
|
|
153
|
+
if (!uiResource?.text) return '';
|
|
154
|
+
|
|
155
|
+
// Only inject window.openai for OpenAI format resources
|
|
156
|
+
if (uiResource.isOpenAiFormat && uiResource.toolInput && uiResource.toolOutput) {
|
|
157
|
+
const openAiScript = generateOpenAiScript(uiResource.toolInput, uiResource.toolOutput, theme);
|
|
158
|
+
const html = uiResource.text;
|
|
159
|
+
|
|
160
|
+
// Inject the script at the beginning of <head> or before <body>
|
|
161
|
+
if (html.includes('<head>')) {
|
|
162
|
+
return html.replace('<head>', '<head>' + openAiScript);
|
|
163
|
+
} else if (html.includes('<body>')) {
|
|
164
|
+
return html.replace('<body>', openAiScript + '<body>');
|
|
165
|
+
} else {
|
|
166
|
+
// No head or body tag, prepend the script
|
|
167
|
+
return openAiScript + html;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return uiResource.text;
|
|
172
|
+
}, [uiResource, theme]);
|
|
173
|
+
|
|
174
|
+
const hasHtmlResource = uiResource?.text &&
|
|
175
|
+
(uiResource.mimeType?.includes('html') || uiResource.text.trim().startsWith('<'));
|
|
176
|
+
|
|
177
|
+
if (!hasHtmlResource) return null;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<iframe
|
|
181
|
+
srcDoc={iframeSrcDoc}
|
|
182
|
+
sandbox="allow-scripts allow-same-origin allow-popups"
|
|
183
|
+
className="w-full bg-background"
|
|
184
|
+
style={{ minHeight: '200px', border: 'none' }}
|
|
185
|
+
onLoad={(e) => {
|
|
186
|
+
// Auto-resize iframe to fit content
|
|
187
|
+
const iframe = e.target as HTMLIFrameElement;
|
|
188
|
+
try {
|
|
189
|
+
const height = iframe.contentDocument?.body?.scrollHeight;
|
|
190
|
+
if (height) {
|
|
191
|
+
iframe.style.height = `${Math.min(height + 20, 600)}px`;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Cross-origin restriction, keep default height
|
|
195
|
+
}
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default function ToolCallDisplay({ toolCall, toolResult, isPending, uiResource, hideUiResource, autoApproveReason }: ToolCallDisplayProps) {
|
|
202
|
+
// Check if we have HTML content to render
|
|
203
|
+
const hasHtmlResource = !hideUiResource && uiResource?.text &&
|
|
204
|
+
(uiResource.mimeType?.includes('html') || uiResource.text.trim().startsWith('<'));
|
|
205
|
+
|
|
206
|
+
// Auto-expand if we have HTML to render, or start collapsed if hiding UI resource
|
|
207
|
+
const [isExpanded, setIsExpanded] = useState(hasHtmlResource);
|
|
208
|
+
const [showRawResult, setShowRawResult] = useState(false);
|
|
209
|
+
|
|
210
|
+
// Update expanded state when uiResource changes (e.g., after streaming completes)
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (hasHtmlResource) {
|
|
213
|
+
setIsExpanded(true);
|
|
214
|
+
}
|
|
215
|
+
}, [hasHtmlResource]);
|
|
216
|
+
|
|
217
|
+
// Get current theme from document
|
|
218
|
+
const theme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
219
|
+
|
|
220
|
+
// Prepare iframe content with window.openai injection for OpenAI format
|
|
221
|
+
const iframeSrcDoc = useMemo(() => {
|
|
222
|
+
if (!uiResource?.text || hideUiResource) return '';
|
|
223
|
+
|
|
224
|
+
// Only inject window.openai for OpenAI format resources
|
|
225
|
+
if (uiResource.isOpenAiFormat && uiResource.toolInput && uiResource.toolOutput) {
|
|
226
|
+
const openAiScript = generateOpenAiScript(uiResource.toolInput, uiResource.toolOutput, theme);
|
|
227
|
+
const html = uiResource.text;
|
|
228
|
+
|
|
229
|
+
// Inject the script at the beginning of <head> or before <body>
|
|
230
|
+
if (html.includes('<head>')) {
|
|
231
|
+
return html.replace('<head>', '<head>' + openAiScript);
|
|
232
|
+
} else if (html.includes('<body>')) {
|
|
233
|
+
return html.replace('<body>', openAiScript + '<body>');
|
|
234
|
+
} else {
|
|
235
|
+
// No head or body tag, prepend the script
|
|
236
|
+
return openAiScript + html;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return uiResource.text;
|
|
241
|
+
}, [uiResource, hideUiResource, theme]);
|
|
242
|
+
|
|
243
|
+
const isError = toolResult?.isError || toolCall.status === 'error';
|
|
244
|
+
const isCompleted = toolResult || toolCall.status === 'completed';
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className={`my-2 rounded-lg border ${isError ? 'border-error/30 bg-error/5' : 'border-border bg-background-secondary/50'}`}>
|
|
248
|
+
{/* Header */}
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
251
|
+
className="flex w-full items-center gap-2 p-3 text-left hover:bg-background-secondary/30 transition-colors"
|
|
252
|
+
>
|
|
253
|
+
<div className={`rounded p-1 ${isError ? 'bg-error/10 text-error' : 'bg-primary/10 text-primary'}`}>
|
|
254
|
+
<Wrench size={14} />
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<span className="flex-1 font-medium text-sm text-text-primary">
|
|
258
|
+
{toolCall.toolName}
|
|
259
|
+
</span>
|
|
260
|
+
|
|
261
|
+
{/* Auto-approved indicator */}
|
|
262
|
+
{autoApproveReason && (
|
|
263
|
+
<span
|
|
264
|
+
className="inline-flex items-center gap-1 text-xs text-success bg-success/10 px-2 py-0.5 rounded-full"
|
|
265
|
+
title={AUTO_APPROVE_LABELS[autoApproveReason]}
|
|
266
|
+
>
|
|
267
|
+
<ShieldCheck size={12} />
|
|
268
|
+
<span className="hidden sm:inline">Auto-approved</span>
|
|
269
|
+
</span>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{isPending ? (
|
|
273
|
+
<Loader2 size={16} className="animate-spin text-primary" />
|
|
274
|
+
) : isError ? (
|
|
275
|
+
<XCircle size={16} className="text-error" />
|
|
276
|
+
) : isCompleted ? (
|
|
277
|
+
<CheckCircle size={16} className="text-success" />
|
|
278
|
+
) : null}
|
|
279
|
+
|
|
280
|
+
{isExpanded ? (
|
|
281
|
+
<ChevronDown size={16} className="text-text-muted" />
|
|
282
|
+
) : (
|
|
283
|
+
<ChevronRight size={16} className="text-text-muted" />
|
|
284
|
+
)}
|
|
285
|
+
</button>
|
|
286
|
+
|
|
287
|
+
{/* Expanded Content */}
|
|
288
|
+
{isExpanded && (
|
|
289
|
+
<div className="border-t border-border px-3 pb-3">
|
|
290
|
+
{/* Arguments */}
|
|
291
|
+
{toolCall.arguments && Object.keys(toolCall.arguments).length > 0 && (
|
|
292
|
+
<div className="mt-3">
|
|
293
|
+
<div className="mb-1 text-xs font-medium text-text-muted uppercase tracking-wide">
|
|
294
|
+
Arguments
|
|
295
|
+
</div>
|
|
296
|
+
<pre className="rounded bg-background-secondary p-2 text-xs text-text-secondary overflow-x-auto">
|
|
297
|
+
{JSON.stringify(toolCall.arguments, null, 2)}
|
|
298
|
+
</pre>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* UI Resource (HTML rendered in iframe) */}
|
|
303
|
+
{hasHtmlResource && (
|
|
304
|
+
<div className="mt-3">
|
|
305
|
+
<div className="mb-1 flex items-center justify-between">
|
|
306
|
+
<span className="text-xs font-medium text-text-muted uppercase tracking-wide">
|
|
307
|
+
Output
|
|
308
|
+
</span>
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => setShowRawResult(!showRawResult)}
|
|
311
|
+
className="text-xs text-primary hover:underline"
|
|
312
|
+
>
|
|
313
|
+
{showRawResult ? 'Show rendered' : 'Show raw'}
|
|
314
|
+
</button>
|
|
315
|
+
</div>
|
|
316
|
+
{showRawResult ? (
|
|
317
|
+
<div className="rounded bg-background-secondary p-2 text-sm text-text-secondary">
|
|
318
|
+
<ToolResultContent content={toolResult?.content || []} />
|
|
319
|
+
</div>
|
|
320
|
+
) : (
|
|
321
|
+
<div className="rounded border border-border overflow-hidden">
|
|
322
|
+
<iframe
|
|
323
|
+
srcDoc={iframeSrcDoc}
|
|
324
|
+
sandbox="allow-scripts allow-same-origin allow-popups"
|
|
325
|
+
className="w-full bg-background"
|
|
326
|
+
style={{ minHeight: '200px', border: 'none' }}
|
|
327
|
+
onLoad={(e) => {
|
|
328
|
+
// Auto-resize iframe to fit content
|
|
329
|
+
const iframe = e.target as HTMLIFrameElement;
|
|
330
|
+
try {
|
|
331
|
+
const height = iframe.contentDocument?.body?.scrollHeight;
|
|
332
|
+
if (height) {
|
|
333
|
+
iframe.style.height = `${Math.min(height + 20, 600)}px`;
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
// Cross-origin restriction, keep default height
|
|
337
|
+
}
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
|
|
345
|
+
{/* Regular Result (when no UI resource) */}
|
|
346
|
+
{toolResult && !hasHtmlResource && (
|
|
347
|
+
<div className="mt-3">
|
|
348
|
+
<div className="mb-1 text-xs font-medium text-text-muted uppercase tracking-wide">
|
|
349
|
+
{isError ? 'Error' : 'Result'}
|
|
350
|
+
</div>
|
|
351
|
+
<div className={`rounded p-2 text-sm ${isError ? 'bg-error/10 text-error' : 'bg-background-secondary text-text-secondary'}`}>
|
|
352
|
+
<ToolResultContent content={toolResult.content} />
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function ToolResultContent({ content }: { content: MCPContent[] }) {
|
|
363
|
+
if (!content || content.length === 0) {
|
|
364
|
+
return <span className="text-text-muted italic">No output</span>;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div className="space-y-2">
|
|
369
|
+
{content.map((item, index) => (
|
|
370
|
+
<div key={index}>
|
|
371
|
+
{item.type === 'text' && (
|
|
372
|
+
<pre className="whitespace-pre-wrap font-mono text-xs overflow-x-auto">
|
|
373
|
+
{item.text}
|
|
374
|
+
</pre>
|
|
375
|
+
)}
|
|
376
|
+
{item.type === 'image' && item.data && (
|
|
377
|
+
<img
|
|
378
|
+
src={`data:${item.mimeType || 'image/png'};base64,${item.data}`}
|
|
379
|
+
alt="Tool result"
|
|
380
|
+
className="max-w-full rounded"
|
|
381
|
+
/>
|
|
382
|
+
)}
|
|
383
|
+
{item.type === 'resource' && item.resource && (
|
|
384
|
+
<div className="rounded border border-border p-2">
|
|
385
|
+
<div className="text-xs text-text-muted">{item.resource.uri}</div>
|
|
386
|
+
{item.resource.text && (
|
|
387
|
+
<pre className="mt-1 whitespace-pre-wrap font-mono text-xs">
|
|
388
|
+
{item.resource.text}
|
|
389
|
+
</pre>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
393
|
+
{item.type === 'resource_link' && item.uri && (
|
|
394
|
+
<a
|
|
395
|
+
href={item.uri}
|
|
396
|
+
target="_blank"
|
|
397
|
+
rel="noopener noreferrer"
|
|
398
|
+
className="text-primary hover:underline text-sm"
|
|
399
|
+
>
|
|
400
|
+
{item.name || item.uri}
|
|
401
|
+
</a>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Component for displaying pending tool calls during streaming
|
|
410
|
+
interface PendingToolCallsProps {
|
|
411
|
+
pendingCalls: Array<{
|
|
412
|
+
id: string;
|
|
413
|
+
name: string;
|
|
414
|
+
serverId: string;
|
|
415
|
+
input: Record<string, unknown>;
|
|
416
|
+
}>;
|
|
417
|
+
completedCalls: Array<{
|
|
418
|
+
id: string;
|
|
419
|
+
name: string;
|
|
420
|
+
serverId: string;
|
|
421
|
+
input: Record<string, unknown>;
|
|
422
|
+
result: MCPContent[];
|
|
423
|
+
isError?: boolean;
|
|
424
|
+
uiResource?: UIResource;
|
|
425
|
+
autoApproveReason?: AutoApproveReason;
|
|
426
|
+
}>;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function PendingToolCalls({ pendingCalls, completedCalls }: PendingToolCallsProps) {
|
|
430
|
+
if (pendingCalls.length === 0 && completedCalls.length === 0) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div className="my-2 space-y-1">
|
|
436
|
+
{/* Show completed calls first */}
|
|
437
|
+
{completedCalls.map((call) => (
|
|
438
|
+
<ToolCallDisplay
|
|
439
|
+
key={call.id}
|
|
440
|
+
toolCall={{
|
|
441
|
+
id: call.id,
|
|
442
|
+
serverId: call.serverId,
|
|
443
|
+
toolName: call.name,
|
|
444
|
+
arguments: call.input,
|
|
445
|
+
status: call.isError ? 'error' : 'completed',
|
|
446
|
+
}}
|
|
447
|
+
toolResult={{
|
|
448
|
+
toolCallId: call.id,
|
|
449
|
+
content: call.result,
|
|
450
|
+
isError: call.isError,
|
|
451
|
+
}}
|
|
452
|
+
uiResource={call.uiResource}
|
|
453
|
+
autoApproveReason={call.autoApproveReason}
|
|
454
|
+
/>
|
|
455
|
+
))}
|
|
456
|
+
|
|
457
|
+
{/* Show pending calls */}
|
|
458
|
+
{pendingCalls.map((call) => (
|
|
459
|
+
<ToolCallDisplay
|
|
460
|
+
key={call.id}
|
|
461
|
+
toolCall={{
|
|
462
|
+
id: call.id,
|
|
463
|
+
serverId: call.serverId,
|
|
464
|
+
toolName: call.name,
|
|
465
|
+
arguments: call.input,
|
|
466
|
+
status: 'pending',
|
|
467
|
+
}}
|
|
468
|
+
isPending
|
|
469
|
+
/>
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Wrench, ChevronDown, ChevronRight, X, Shield, ShieldCheck } from 'lucide-react';
|
|
3
|
+
import type { PendingToolConfirmation, ConfirmationScope } from '../stores/chatStore';
|
|
4
|
+
|
|
5
|
+
interface ToolConfirmationModalProps {
|
|
6
|
+
confirmation: PendingToolConfirmation;
|
|
7
|
+
onConfirm: (approved: boolean, scope?: ConfirmationScope) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ToolConfirmationModal({ confirmation, onConfirm }: ToolConfirmationModalProps) {
|
|
11
|
+
const [showArgs, setShowArgs] = useState(false);
|
|
12
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
13
|
+
|
|
14
|
+
const handleConfirm = async (approved: boolean, scope?: ConfirmationScope) => {
|
|
15
|
+
setIsSubmitting(true);
|
|
16
|
+
try {
|
|
17
|
+
onConfirm(approved, scope);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error confirming tool:', error);
|
|
20
|
+
setIsSubmitting(false);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
26
|
+
<div className="bg-background border border-border rounded-xl shadow-xl max-w-md w-full overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
|
27
|
+
{/* Header */}
|
|
28
|
+
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
29
|
+
<div className="flex items-center gap-3">
|
|
30
|
+
<div className="rounded-lg bg-warning/10 p-2 text-warning">
|
|
31
|
+
<Shield size={20} />
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<h3 className="font-semibold text-text-primary">Tool Permission Required</h3>
|
|
35
|
+
<p className="text-sm text-text-secondary">Allow this tool to run?</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => handleConfirm(false)}
|
|
40
|
+
disabled={isSubmitting}
|
|
41
|
+
className="p-1.5 rounded-lg text-text-muted hover:text-text-primary hover:bg-background-secondary transition-colors"
|
|
42
|
+
aria-label="Deny"
|
|
43
|
+
>
|
|
44
|
+
<X size={18} />
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Content */}
|
|
49
|
+
<div className="p-4 space-y-4">
|
|
50
|
+
{/* Tool Info */}
|
|
51
|
+
<div className="flex items-start gap-3 p-3 rounded-lg bg-background-secondary">
|
|
52
|
+
<div className="rounded p-1.5 bg-primary/10 text-primary">
|
|
53
|
+
<Wrench size={16} />
|
|
54
|
+
</div>
|
|
55
|
+
<div className="flex-1 min-w-0">
|
|
56
|
+
<div className="font-medium text-text-primary">{confirmation.toolName}</div>
|
|
57
|
+
<div className="text-sm text-text-muted truncate">
|
|
58
|
+
Server: {confirmation.serverId}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Arguments (collapsible) */}
|
|
64
|
+
{confirmation.toolArgs != null && typeof confirmation.toolArgs === 'object' && Object.keys(confirmation.toolArgs as object).length > 0 && (
|
|
65
|
+
<div className="rounded-lg border border-border overflow-hidden">
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => setShowArgs(!showArgs)}
|
|
68
|
+
className="flex items-center justify-between w-full px-3 py-2 text-sm text-text-secondary hover:bg-background-secondary transition-colors"
|
|
69
|
+
>
|
|
70
|
+
<span>View arguments</span>
|
|
71
|
+
{showArgs ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
|
72
|
+
</button>
|
|
73
|
+
{showArgs && (
|
|
74
|
+
<div className="px-3 py-2 border-t border-border bg-background-secondary/50">
|
|
75
|
+
<pre className="text-xs text-text-secondary overflow-x-auto whitespace-pre-wrap font-mono max-h-48 overflow-y-auto">
|
|
76
|
+
{JSON.stringify(confirmation.toolArgs, null, 2)}
|
|
77
|
+
</pre>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{/* Trust info */}
|
|
84
|
+
<div className="flex items-start gap-2 p-3 rounded-lg bg-primary/5 border border-primary/20">
|
|
85
|
+
<ShieldCheck size={16} className="mt-0.5 text-primary flex-shrink-0" />
|
|
86
|
+
<p className="text-sm text-text-secondary">
|
|
87
|
+
You can allow this tool to run once, for this conversation, or always for future chats.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Actions */}
|
|
93
|
+
<div className="p-4 border-t border-border bg-background-secondary/30">
|
|
94
|
+
<div className="grid grid-cols-2 gap-2 mb-2">
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => handleConfirm(true, 'once')}
|
|
97
|
+
disabled={isSubmitting}
|
|
98
|
+
className="px-4 py-2.5 rounded-lg bg-primary text-white font-medium hover:bg-primary-hover transition-colors disabled:opacity-50"
|
|
99
|
+
>
|
|
100
|
+
Allow once
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => handleConfirm(true, 'thread')}
|
|
104
|
+
disabled={isSubmitting}
|
|
105
|
+
className="px-4 py-2.5 rounded-lg border border-border bg-background text-text-primary font-medium hover:bg-background-secondary transition-colors disabled:opacity-50"
|
|
106
|
+
>
|
|
107
|
+
Allow for this chat
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="grid grid-cols-2 gap-2">
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => handleConfirm(true, 'always')}
|
|
113
|
+
disabled={isSubmitting}
|
|
114
|
+
className="px-4 py-2.5 rounded-lg border border-success/50 bg-success/5 text-success font-medium hover:bg-success/10 transition-colors disabled:opacity-50"
|
|
115
|
+
>
|
|
116
|
+
Always allow
|
|
117
|
+
</button>
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => handleConfirm(false)}
|
|
120
|
+
disabled={isSubmitting}
|
|
121
|
+
className="px-4 py-2.5 rounded-lg border border-error/50 bg-error/5 text-error font-medium hover:bg-error/10 transition-colors disabled:opacity-50"
|
|
122
|
+
>
|
|
123
|
+
Deny
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|