@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.
Files changed (135) hide show
  1. package/dist/favicon.svg +11 -0
  2. package/dist/index.html +17 -0
  3. package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
  4. package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
  5. package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
  6. package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
  7. package/dist/lib/extensions.js +10 -0
  8. package/dist/lib/extensions.js.map +1 -0
  9. package/dist/lib/favicon.svg +11 -0
  10. package/dist/lib/index.js +74126 -0
  11. package/dist/lib/index.js.map +1 -0
  12. package/dist/lib/logo.svg +12 -0
  13. package/dist/lib/routes/AcceptInviteRoute.js +19 -0
  14. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
  15. package/dist/lib/routes/AdminDashboardRoute.js +19 -0
  16. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
  17. package/dist/lib/routes/AdminTeamRoute.js +19 -0
  18. package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
  19. package/dist/lib/routes/AdminTeamsRoute.js +19 -0
  20. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
  21. package/dist/lib/routes/AdminUsersRoute.js +19 -0
  22. package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
  23. package/dist/lib/routes/ApiKeysRoute.js +19 -0
  24. package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
  25. package/dist/lib/routes/AutomationsRoute.js +19 -0
  26. package/dist/lib/routes/AutomationsRoute.js.map +1 -0
  27. package/dist/lib/routes/ChatRoute.js +19 -0
  28. package/dist/lib/routes/ChatRoute.js.map +1 -0
  29. package/dist/lib/routes/DocumentsRoute.js +19 -0
  30. package/dist/lib/routes/DocumentsRoute.js.map +1 -0
  31. package/dist/lib/routes/OAuthConsentRoute.js +19 -0
  32. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
  33. package/dist/lib/routes/PricingRoute.js +19 -0
  34. package/dist/lib/routes/PricingRoute.js.map +1 -0
  35. package/dist/lib/routes/PrivacyRoute.js +19 -0
  36. package/dist/lib/routes/PrivacyRoute.js.map +1 -0
  37. package/dist/lib/routes/TeamSettingsRoute.js +19 -0
  38. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
  39. package/dist/lib/routes/TermsRoute.js +19 -0
  40. package/dist/lib/routes/TermsRoute.js.map +1 -0
  41. package/dist/lib/routes/VerifyEmailRoute.js +19 -0
  42. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
  43. package/dist/lib/routes.js +79 -0
  44. package/dist/lib/routes.js.map +1 -0
  45. package/dist/lib/ssr-utils.js +29 -0
  46. package/dist/lib/ssr-utils.js.map +1 -0
  47. package/dist/lib/ssr.js +60 -0
  48. package/dist/lib/ssr.js.map +1 -0
  49. package/dist/lib/styles.css +2410 -0
  50. package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
  51. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
  52. package/dist/logo.svg +12 -0
  53. package/package.json +84 -0
  54. package/src/components/AgentSelector.tsx +90 -0
  55. package/src/components/BranchModal.tsx +129 -0
  56. package/src/components/ClientOnly.tsx +27 -0
  57. package/src/components/ExportMenu.tsx +122 -0
  58. package/src/components/LoadingSkeletons.tsx +110 -0
  59. package/src/components/MCPCredentialsSection.tsx +309 -0
  60. package/src/components/MentionChip.tsx +149 -0
  61. package/src/components/MentionDropdown.tsx +175 -0
  62. package/src/components/MentionInput.tsx +293 -0
  63. package/src/components/MessageItem.tsx +300 -0
  64. package/src/components/MessageList.tsx +159 -0
  65. package/src/components/OAuthAppsSection.tsx +124 -0
  66. package/src/components/ProjectFolder.tsx +141 -0
  67. package/src/components/ProjectModal.tsx +296 -0
  68. package/src/components/SSRMessageList.tsx +153 -0
  69. package/src/components/SearchModal.tsx +173 -0
  70. package/src/components/SettingsModal.tsx +412 -0
  71. package/src/components/ShareModal.tsx +280 -0
  72. package/src/components/Sidebar.tsx +491 -0
  73. package/src/components/TeamSwitcher.tsx +273 -0
  74. package/src/components/ToolCallDisplay.tsx +473 -0
  75. package/src/components/ToolConfirmationModal.tsx +130 -0
  76. package/src/components/UsageChart.tsx +177 -0
  77. package/src/components/content/CodeBlock.tsx +69 -0
  78. package/src/components/content/MarkdownRenderer.tsx +64 -0
  79. package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
  80. package/src/contexts/AuthContext.tsx +119 -0
  81. package/src/contexts/ConfigContext.tsx +214 -0
  82. package/src/contexts/ProjectContext.tsx +167 -0
  83. package/src/contexts/ServerConfigProvider.tsx +41 -0
  84. package/src/contexts/ServerThemeProvider.tsx +47 -0
  85. package/src/contexts/TeamContext.tsx +255 -0
  86. package/src/contexts/ThemeContext.tsx +113 -0
  87. package/src/extensions/index.ts +15 -0
  88. package/src/extensions/registry.ts +187 -0
  89. package/src/extensions/useExtensions.ts +52 -0
  90. package/src/hooks/useAppPath.ts +34 -0
  91. package/src/hooks/useBasePath.ts +13 -0
  92. package/src/hooks/useKeyboardShortcuts.ts +50 -0
  93. package/src/hooks/useMentionSearch.ts +106 -0
  94. package/src/index.tsx +116 -0
  95. package/src/layouts/MainLayout.tsx +98 -0
  96. package/src/pages/AcceptInvitePage.tsx +175 -0
  97. package/src/pages/AdminDashboardPage.tsx +362 -0
  98. package/src/pages/AdminTeamPage.tsx +304 -0
  99. package/src/pages/AdminTeamsPage.tsx +242 -0
  100. package/src/pages/AdminUsersPage.tsx +385 -0
  101. package/src/pages/ApiKeysPage.tsx +449 -0
  102. package/src/pages/ChatPage.tsx +310 -0
  103. package/src/pages/DocumentsPage.tsx +577 -0
  104. package/src/pages/LoginPage.tsx +232 -0
  105. package/src/pages/OAuthConsentPage.tsx +234 -0
  106. package/src/pages/PricingPage.tsx +314 -0
  107. package/src/pages/PrivacyPage.tsx +65 -0
  108. package/src/pages/RegisterPage.tsx +153 -0
  109. package/src/pages/ScheduledPromptsPage.tsx +702 -0
  110. package/src/pages/SharedThreadPage.tsx +116 -0
  111. package/src/pages/TeamSettingsPage.tsx +1085 -0
  112. package/src/pages/TermsPage.tsx +82 -0
  113. package/src/pages/VerifyEmailPage.tsx +202 -0
  114. package/src/routes/AcceptInviteRoute.tsx +24 -0
  115. package/src/routes/AdminDashboardRoute.tsx +24 -0
  116. package/src/routes/AdminTeamRoute.tsx +24 -0
  117. package/src/routes/AdminTeamsRoute.tsx +24 -0
  118. package/src/routes/AdminUsersRoute.tsx +24 -0
  119. package/src/routes/ApiKeysRoute.tsx +24 -0
  120. package/src/routes/AutomationsRoute.tsx +24 -0
  121. package/src/routes/ChatRoute.tsx +28 -0
  122. package/src/routes/DocumentsRoute.tsx +24 -0
  123. package/src/routes/OAuthConsentRoute.tsx +24 -0
  124. package/src/routes/PricingRoute.tsx +24 -0
  125. package/src/routes/PrivacyRoute.tsx +24 -0
  126. package/src/routes/TeamSettingsRoute.tsx +24 -0
  127. package/src/routes/TermsRoute.tsx +24 -0
  128. package/src/routes/VerifyEmailRoute.tsx +24 -0
  129. package/src/routes/index.ts +57 -0
  130. package/src/ssr-utils.tsx +84 -0
  131. package/src/ssr.ts +123 -0
  132. package/src/stores/chatStore.ts +670 -0
  133. package/src/styles/index.css +254 -0
  134. package/src/utils/api.ts +78 -0
  135. 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
+ }