@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,300 @@
1
+ import { useState } from 'react';
2
+ import { Copy, Check, RefreshCw, ThumbsUp, ThumbsDown, User, Bot, GitBranch } from 'lucide-react';
3
+ import { useNavigate } from 'react-router';
4
+ import type { Message } from '@chaaskit/shared';
5
+ import { useChatStore } from '../stores/chatStore';
6
+ import { useTheme } from '../contexts/ThemeContext';
7
+ import { useConfig } from '../contexts/ConfigContext';
8
+ import { useAppPath } from '../hooks/useAppPath';
9
+ import MarkdownRenderer from './content/MarkdownRenderer';
10
+ import ToolCallDisplay, { UIResourceWidget } from './ToolCallDisplay';
11
+ import BranchModal from './BranchModal';
12
+ import { MessageContentWithMentions } from './MentionChip';
13
+
14
+ interface MessageItemProps {
15
+ message: Message;
16
+ isStreaming?: boolean;
17
+ messageIndex?: number;
18
+ previousMessage?: Message;
19
+ }
20
+
21
+ export default function MessageItem({ message, isStreaming, messageIndex = 0, previousMessage }: MessageItemProps) {
22
+ const [copied, setCopied] = useState(false);
23
+ const [showBranchModal, setShowBranchModal] = useState(false);
24
+ const [feedback, setFeedback] = useState<'up' | 'down' | null>(null);
25
+ const { regenerateMessage, branchFromMessage, sendMessage, isStreaming: isGlobalStreaming } = useChatStore();
26
+ const { theme } = useTheme();
27
+ const config = useConfig();
28
+ const navigate = useNavigate();
29
+ const appPath = useAppPath();
30
+
31
+ const isUser = message.role === 'user';
32
+ const showToolCalls = config.mcp?.showToolCalls !== false;
33
+
34
+ // Branching logic:
35
+ // - Can't branch from first message (nothing to branch from)
36
+ // - For user messages: branch from previous message, pre-fill with current content
37
+ // - For assistant messages: branch from this message
38
+ const canBranch = messageIndex > 0;
39
+ const branchTargetMessage = isUser ? previousMessage : message;
40
+ const branchInitialContent = isUser ? message.content : '';
41
+
42
+ async function handleCopy() {
43
+ await navigator.clipboard.writeText(message.content);
44
+ setCopied(true);
45
+ setTimeout(() => setCopied(false), 2000);
46
+ }
47
+
48
+ async function handleRegenerate() {
49
+ if (isGlobalStreaming) return;
50
+ await regenerateMessage(message.id);
51
+ }
52
+
53
+ async function handleBranch(content?: string) {
54
+ if (!branchTargetMessage) return;
55
+ // Create the branch (without the new message)
56
+ const newThread = await branchFromMessage(branchTargetMessage.id);
57
+ navigate(appPath(`/thread/${newThread.id}`));
58
+ // If there's content, send it as a message to trigger AI response
59
+ if (content) {
60
+ await sendMessage(content);
61
+ }
62
+ }
63
+
64
+ async function handleFeedback(type: 'up' | 'down') {
65
+ // Toggle off if clicking the same feedback
66
+ const newFeedback = feedback === type ? null : type;
67
+ setFeedback(newFeedback);
68
+
69
+ if (newFeedback) {
70
+ try {
71
+ await fetch(`/api/chat/feedback/${message.id}`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ type: newFeedback }),
75
+ credentials: 'include',
76
+ });
77
+ } catch (error) {
78
+ console.error('Failed to submit feedback:', error);
79
+ // Revert on error
80
+ setFeedback(feedback);
81
+ }
82
+ }
83
+ }
84
+
85
+ // For assistant messages, collect tool calls with UI resources
86
+ const toolCallsWithResults = !isUser && message.toolCalls?.map((toolCall) => ({
87
+ toolCall,
88
+ toolResult: message.toolResults?.find((r) => r.toolCallId === toolCall.id),
89
+ })) || [];
90
+
91
+ // Check if any tool has a UI resource to render
92
+ const uiResources = toolCallsWithResults
93
+ .filter((tc) => tc.toolResult?.uiResource?.text)
94
+ .map((tc) => tc.toolResult!.uiResource!);
95
+
96
+ // Debug logging
97
+ if (!isUser && message.toolCalls?.length) {
98
+ console.log('[MessageItem] Rendering message with toolCalls:', message.toolCalls.length);
99
+ console.log('[MessageItem] toolResults:', message.toolResults?.length || 0, message.toolResults?.map(tr => ({ id: tr.toolCallId, hasUiResource: !!tr.uiResource, textLen: tr.uiResource?.text?.length })));
100
+ console.log('[MessageItem] uiResources to render:', uiResources.length);
101
+ }
102
+
103
+ // User messages render normally
104
+ if (isUser) {
105
+ return (
106
+ <>
107
+ <div className="group flex gap-3 flex-row-reverse animate-fade-in">
108
+ {/* Avatar */}
109
+ <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary">
110
+ <User size={12} className="text-white" />
111
+ </div>
112
+
113
+ {/* Message Content */}
114
+ <div className="flex max-w-[85%] flex-col sm:max-w-[80%] items-end">
115
+ <div className="rounded-lg px-3 py-2 sm:px-3 sm:py-2 bg-user-message-bg text-user-message-text">
116
+ <p className="whitespace-pre-wrap text-sm">
117
+ <MessageContentWithMentions content={message.content} />
118
+ </p>
119
+ </div>
120
+
121
+ {/* File Attachments */}
122
+ {message.files && message.files.length > 0 && (
123
+ <div className="mt-2 flex flex-wrap gap-2">
124
+ {message.files.map((file) => (
125
+ <div
126
+ key={file.id}
127
+ className="rounded-lg bg-background-secondary px-3 py-1 text-sm text-text-secondary"
128
+ >
129
+ {file.name}
130
+ </div>
131
+ ))}
132
+ </div>
133
+ )}
134
+
135
+ {/* Action Buttons */}
136
+ <div className="mt-2 flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 touch-device:opacity-100 sm:gap-1">
137
+ <button
138
+ onClick={handleCopy}
139
+ className="rounded p-1.5 text-text-muted hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary sm:p-1"
140
+ title="Copy"
141
+ aria-label="Copy message"
142
+ >
143
+ {copied ? <Check size={16} className="sm:h-[14px] sm:w-[14px]" /> : <Copy size={16} className="sm:h-[14px] sm:w-[14px]" />}
144
+ </button>
145
+ {canBranch && (
146
+ <button
147
+ onClick={() => setShowBranchModal(true)}
148
+ disabled={isGlobalStreaming}
149
+ className="rounded p-1.5 text-text-muted hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary disabled:opacity-50 sm:p-1"
150
+ title="Edit and resend this message"
151
+ aria-label="Branch conversation with edited message"
152
+ >
153
+ <GitBranch size={16} className="sm:h-[14px] sm:w-[14px]" />
154
+ </button>
155
+ )}
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ {/* Branch Modal - for user messages, we branch from previous message */}
161
+ {canBranch && branchTargetMessage && (
162
+ <BranchModal
163
+ isOpen={showBranchModal}
164
+ onClose={() => setShowBranchModal(false)}
165
+ onBranch={handleBranch}
166
+ messagePreview={branchTargetMessage.content.slice(0, 200) + (branchTargetMessage.content.length > 200 ? '...' : '')}
167
+ initialContent={branchInitialContent}
168
+ />
169
+ )}
170
+ </>
171
+ );
172
+ }
173
+
174
+ // Assistant messages: Tool calls → UI widgets → Text response
175
+ return (
176
+ <div className="animate-fade-in space-y-3">
177
+ {/* 1. Tool Execution Cards (outside bubble) */}
178
+ {showToolCalls && toolCallsWithResults.length > 0 && (
179
+ <div className="space-y-2">
180
+ {toolCallsWithResults.map(({ toolCall, toolResult }) => (
181
+ <ToolCallDisplay
182
+ key={toolCall.id}
183
+ toolCall={toolCall}
184
+ toolResult={toolResult}
185
+ hideUiResource
186
+ />
187
+ ))}
188
+ </div>
189
+ )}
190
+
191
+ {/* 2. UI Resource Widgets (outside bubble, full width) */}
192
+ {uiResources.length > 0 && (
193
+ <div className="space-y-3">
194
+ {uiResources.map((uiResource, index) => (
195
+ <UIResourceWidget key={index} uiResource={uiResource} theme={theme} />
196
+ ))}
197
+ </div>
198
+ )}
199
+
200
+ {/* 3. Text Response Bubble (with avatar) */}
201
+ {(message.content || isStreaming) && (
202
+ <div className="group flex gap-3">
203
+ {/* Avatar */}
204
+ <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-secondary overflow-hidden">
205
+ {config.ui?.logo ? (
206
+ <img src={typeof config.ui.logo === 'string' ? config.ui.logo : (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light)} alt="" className="h-full w-full object-cover" />
207
+ ) : (
208
+ <Bot size={12} className="text-white" />
209
+ )}
210
+ </div>
211
+
212
+ {/* Message Content */}
213
+ <div className="flex max-w-[85%] flex-col sm:max-w-[80%] items-start">
214
+ <div className="rounded-lg px-3 py-2 sm:px-3 sm:py-2 bg-assistant-message-bg text-assistant-message-text">
215
+ <div className="markdown-content text-sm">
216
+ <MarkdownRenderer content={message.content} />
217
+ {isStreaming && (
218
+ <span className="typing-indicator ml-1">
219
+ <span className="inline-block h-2 w-2 rounded-full bg-current" />
220
+ <span className="ml-1 inline-block h-2 w-2 rounded-full bg-current" />
221
+ <span className="ml-1 inline-block h-2 w-2 rounded-full bg-current" />
222
+ </span>
223
+ )}
224
+ </div>
225
+ </div>
226
+
227
+ {/* Action Buttons */}
228
+ {!isStreaming && (
229
+ <div className="mt-2 flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 touch-device:opacity-100 sm:gap-1">
230
+ <button
231
+ onClick={handleCopy}
232
+ className="rounded p-1.5 text-text-muted hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary sm:p-1"
233
+ title="Copy"
234
+ aria-label="Copy message"
235
+ >
236
+ {copied ? <Check size={16} className="sm:h-[14px] sm:w-[14px]" /> : <Copy size={16} className="sm:h-[14px] sm:w-[14px]" />}
237
+ </button>
238
+ <button
239
+ onClick={handleRegenerate}
240
+ disabled={isGlobalStreaming}
241
+ className="rounded p-1.5 text-text-muted hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary disabled:opacity-50 sm:p-1"
242
+ title="Regenerate"
243
+ aria-label="Regenerate response"
244
+ >
245
+ <RefreshCw size={16} className="sm:h-[14px] sm:w-[14px]" />
246
+ </button>
247
+ {canBranch && (
248
+ <button
249
+ onClick={() => setShowBranchModal(true)}
250
+ disabled={isGlobalStreaming}
251
+ className="rounded p-1.5 text-text-muted hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary disabled:opacity-50 sm:p-1"
252
+ title="Branch from here"
253
+ aria-label="Branch conversation from this message"
254
+ >
255
+ <GitBranch size={16} className="sm:h-[14px] sm:w-[14px]" />
256
+ </button>
257
+ )}
258
+ <button
259
+ onClick={() => handleFeedback('up')}
260
+ className={`rounded p-1.5 sm:p-1 ${
261
+ feedback === 'up'
262
+ ? 'bg-success/20 text-success'
263
+ : 'text-text-muted hover:bg-success/10 hover:text-success active:bg-success/10'
264
+ }`}
265
+ title="Good response"
266
+ aria-label="Mark as good response"
267
+ >
268
+ <ThumbsUp size={16} className="sm:h-[14px] sm:w-[14px]" />
269
+ </button>
270
+ <button
271
+ onClick={() => handleFeedback('down')}
272
+ className={`rounded p-1.5 sm:p-1 ${
273
+ feedback === 'down'
274
+ ? 'bg-error/20 text-error'
275
+ : 'text-text-muted hover:bg-error/10 hover:text-error active:bg-error/10'
276
+ }`}
277
+ title="Bad response"
278
+ aria-label="Mark as bad response"
279
+ >
280
+ <ThumbsDown size={16} className="sm:h-[14px] sm:w-[14px]" />
281
+ </button>
282
+ </div>
283
+ )}
284
+ </div>
285
+ </div>
286
+ )}
287
+
288
+ {/* Branch Modal */}
289
+ {canBranch && branchTargetMessage && (
290
+ <BranchModal
291
+ isOpen={showBranchModal}
292
+ onClose={() => setShowBranchModal(false)}
293
+ onBranch={handleBranch}
294
+ messagePreview={branchTargetMessage.content.slice(0, 200) + (branchTargetMessage.content.length > 200 ? '...' : '')}
295
+ initialContent={branchInitialContent}
296
+ />
297
+ )}
298
+ </div>
299
+ );
300
+ }
@@ -0,0 +1,159 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import type { Message, MCPContent, UIResource } from '@chaaskit/shared';
3
+ import { Bot } from 'lucide-react';
4
+ import { useTheme } from '../contexts/ThemeContext';
5
+ import { useConfig } from '../contexts/ConfigContext';
6
+ import MessageItem from './MessageItem';
7
+ import ToolCallDisplay, { UIResourceWidget } from './ToolCallDisplay';
8
+
9
+ interface PendingToolCall {
10
+ id: string;
11
+ name: string;
12
+ serverId: string;
13
+ input: Record<string, unknown>;
14
+ }
15
+
16
+ interface CompletedToolCall extends PendingToolCall {
17
+ result: MCPContent[];
18
+ isError?: boolean;
19
+ uiResource?: UIResource;
20
+ }
21
+
22
+ interface MessageListProps {
23
+ messages: Message[];
24
+ streamingContent?: string;
25
+ pendingToolCalls?: PendingToolCall[];
26
+ completedToolCalls?: CompletedToolCall[];
27
+ }
28
+
29
+ export default function MessageList({
30
+ messages,
31
+ streamingContent,
32
+ pendingToolCalls = [],
33
+ completedToolCalls = [],
34
+ }: MessageListProps) {
35
+ const bottomRef = useRef<HTMLDivElement>(null);
36
+ const { theme } = useTheme();
37
+ const config = useConfig();
38
+ const showToolCalls = config.mcp?.showToolCalls !== false;
39
+
40
+ useEffect(() => {
41
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
42
+ }, [messages, streamingContent, pendingToolCalls, completedToolCalls]);
43
+
44
+ const hasToolActivity = pendingToolCalls.length > 0 || completedToolCalls.length > 0;
45
+ const isStreaming = Boolean(streamingContent) || hasToolActivity;
46
+
47
+ // Get UI resources from completed tool calls for separate rendering
48
+ const uiResources = completedToolCalls
49
+ .filter((tc) => tc.uiResource?.text)
50
+ .map((tc) => tc.uiResource!);
51
+
52
+ // Debug logging
53
+ console.log('[MessageList] Rendering messages:', messages.length, 'isStreaming:', isStreaming);
54
+ messages.forEach((msg, i) => {
55
+ if (msg.role === 'assistant' && msg.toolCalls?.length) {
56
+ console.log(`[MessageList] Message ${i} has toolCalls:`, msg.toolCalls.length, 'toolResults:', msg.toolResults?.length || 0);
57
+ console.log(`[MessageList] Message ${i} uiResources:`, msg.toolResults?.filter(tr => tr.uiResource?.text).length || 0);
58
+ }
59
+ });
60
+ if (completedToolCalls.length > 0) {
61
+ console.log('[MessageList] Streaming completedToolCalls:', completedToolCalls.length);
62
+ console.log('[MessageList] completedToolCalls uiResources:', completedToolCalls.map(tc => ({ name: tc.name, hasUiResource: !!tc.uiResource, textLen: tc.uiResource?.text?.length })));
63
+ console.log('[MessageList] uiResources to render:', uiResources.length);
64
+ }
65
+
66
+ return (
67
+ <div className="mx-auto max-w-3xl px-3 py-3 sm:px-4 sm:py-6">
68
+ <div className="space-y-3 sm:space-y-4">
69
+ {messages.map((message, index) => (
70
+ <MessageItem
71
+ key={message.id}
72
+ message={message}
73
+ messageIndex={index}
74
+ previousMessage={index > 0 ? messages[index - 1] : undefined}
75
+ />
76
+ ))}
77
+
78
+ {/* Streaming message: Tool calls → UI widgets → Text response */}
79
+ {isStreaming && (
80
+ <div className="animate-fade-in space-y-3">
81
+ {/* 1. Tool Execution Cards (outside bubble) */}
82
+ {showToolCalls && hasToolActivity && (
83
+ <div className="space-y-2">
84
+ {/* Completed tool calls */}
85
+ {completedToolCalls.map((call) => (
86
+ <ToolCallDisplay
87
+ key={call.id}
88
+ toolCall={{
89
+ id: call.id,
90
+ serverId: call.serverId,
91
+ toolName: call.name,
92
+ arguments: call.input,
93
+ status: call.isError ? 'error' : 'completed',
94
+ }}
95
+ toolResult={{
96
+ toolCallId: call.id,
97
+ content: call.result,
98
+ isError: call.isError,
99
+ }}
100
+ hideUiResource
101
+ />
102
+ ))}
103
+
104
+ {/* Pending tool calls */}
105
+ {pendingToolCalls.map((call) => (
106
+ <ToolCallDisplay
107
+ key={call.id}
108
+ toolCall={{
109
+ id: call.id,
110
+ serverId: call.serverId,
111
+ toolName: call.name,
112
+ arguments: call.input,
113
+ status: 'pending',
114
+ }}
115
+ isPending
116
+ />
117
+ ))}
118
+ </div>
119
+ )}
120
+
121
+ {/* 2. UI Resource Widgets (outside bubble, full width) */}
122
+ {uiResources.length > 0 && (
123
+ <div className="space-y-3">
124
+ {uiResources.map((uiResource, index) => (
125
+ <UIResourceWidget key={index} uiResource={uiResource} theme={theme} />
126
+ ))}
127
+ </div>
128
+ )}
129
+
130
+ {/* 3. Text Response Bubble (with avatar) */}
131
+ {streamingContent && (
132
+ <div className="group flex gap-3">
133
+ {/* Avatar */}
134
+ <div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-secondary">
135
+ <Bot size={12} className="text-white" />
136
+ </div>
137
+
138
+ {/* Content */}
139
+ <div className="flex max-w-[85%] flex-col sm:max-w-[80%] items-start">
140
+ <div className="rounded-lg px-3 py-2 sm:px-3 sm:py-2 bg-assistant-message-bg text-assistant-message-text">
141
+ <div className="markdown-content text-sm">
142
+ <span className="whitespace-pre-wrap">{streamingContent}</span>
143
+ <span className="typing-indicator ml-1">
144
+ <span className="inline-block h-2 w-2 rounded-full bg-current" />
145
+ <span className="ml-1 inline-block h-2 w-2 rounded-full bg-current" />
146
+ <span className="ml-1 inline-block h-2 w-2 rounded-full bg-current" />
147
+ </span>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ )}
153
+ </div>
154
+ )}
155
+ </div>
156
+ <div ref={bottomRef} />
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,124 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Loader2, ExternalLink, Trash2, Shield } from 'lucide-react';
3
+ import { useConfig } from '../contexts/ConfigContext';
4
+ import { api } from '../utils/api';
5
+
6
+ interface OAuthApp {
7
+ clientId: string;
8
+ clientName: string;
9
+ clientUri?: string;
10
+ scope?: string;
11
+ authorizedAt: string;
12
+ }
13
+
14
+ export default function OAuthAppsSection() {
15
+ const config = useConfig();
16
+ const [apps, setApps] = useState<OAuthApp[]>([]);
17
+ const [isLoading, setIsLoading] = useState(false);
18
+ const [revokingId, setRevokingId] = useState<string | null>(null);
19
+
20
+ // Check if OAuth is enabled
21
+ const oauthEnabled = config.mcp?.server?.oauth?.enabled;
22
+
23
+ useEffect(() => {
24
+ if (oauthEnabled) {
25
+ loadApps();
26
+ }
27
+ }, [oauthEnabled]);
28
+
29
+ async function loadApps() {
30
+ setIsLoading(true);
31
+ try {
32
+ const response = await api.get<{ apps: OAuthApp[] }>('/api/oauth/apps');
33
+ setApps(response.apps);
34
+ } catch (error) {
35
+ console.error('Failed to load OAuth apps:', error);
36
+ } finally {
37
+ setIsLoading(false);
38
+ }
39
+ }
40
+
41
+ async function handleRevoke(clientId: string) {
42
+ setRevokingId(clientId);
43
+ try {
44
+ await api.delete(`/api/oauth/apps/${clientId}`);
45
+ setApps((prev) => prev.filter((app) => app.clientId !== clientId));
46
+ } catch (error) {
47
+ console.error('Failed to revoke app:', error);
48
+ } finally {
49
+ setRevokingId(null);
50
+ }
51
+ }
52
+
53
+ // Don't render if OAuth is not enabled
54
+ if (!oauthEnabled) {
55
+ return null;
56
+ }
57
+
58
+ return (
59
+ <div className="rounded-lg border border-border bg-background-secondary p-4">
60
+ <div className="mb-3 flex items-center gap-2">
61
+ <Shield size={18} className="text-primary" />
62
+ <h3 className="font-medium text-text-primary">Connected Applications</h3>
63
+ </div>
64
+
65
+ {isLoading ? (
66
+ <div className="flex items-center justify-center py-4">
67
+ <Loader2 className="h-5 w-5 animate-spin text-text-muted" />
68
+ </div>
69
+ ) : apps.length === 0 ? (
70
+ <p className="text-sm text-text-muted">
71
+ No applications have been authorized to access your account.
72
+ </p>
73
+ ) : (
74
+ <div className="space-y-3">
75
+ {apps.map((app) => (
76
+ <div
77
+ key={app.clientId}
78
+ className="flex items-center justify-between rounded-lg border border-border bg-background p-3"
79
+ >
80
+ <div className="min-w-0 flex-1">
81
+ <div className="flex items-center gap-2">
82
+ <span className="font-medium text-text-primary truncate">
83
+ {app.clientName}
84
+ </span>
85
+ {app.clientUri && (
86
+ <a
87
+ href={app.clientUri}
88
+ target="_blank"
89
+ rel="noopener noreferrer"
90
+ className="text-text-muted hover:text-primary"
91
+ >
92
+ <ExternalLink size={14} />
93
+ </a>
94
+ )}
95
+ </div>
96
+ <div className="mt-0.5 text-xs text-text-muted">
97
+ Authorized {new Date(app.authorizedAt).toLocaleDateString()}
98
+ {app.scope && (
99
+ <span className="ml-2">
100
+ Scopes: {app.scope}
101
+ </span>
102
+ )}
103
+ </div>
104
+ </div>
105
+
106
+ <button
107
+ onClick={() => handleRevoke(app.clientId)}
108
+ disabled={revokingId === app.clientId}
109
+ className="ml-3 flex items-center gap-1 rounded-lg px-2 py-1 text-xs text-error hover:bg-error/10 disabled:opacity-50"
110
+ >
111
+ {revokingId === app.clientId ? (
112
+ <Loader2 size={12} className="animate-spin" />
113
+ ) : (
114
+ <Trash2 size={12} />
115
+ )}
116
+ Revoke
117
+ </button>
118
+ </div>
119
+ ))}
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ }