@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,670 @@
1
+ import { create } from 'zustand';
2
+ import type { Thread, ThreadSummary, Message, StreamingMessage, ToolCall, ToolResult, MCPContent, UIResource, AutoApproveReason } from '@chaaskit/shared';
3
+ import { api } from '../utils/api';
4
+
5
+ export interface AgentInfo {
6
+ id: string;
7
+ name: string;
8
+ isDefault?: boolean;
9
+ }
10
+
11
+ interface PendingToolCall {
12
+ id: string;
13
+ name: string;
14
+ serverId: string;
15
+ input: Record<string, unknown>;
16
+ }
17
+
18
+ interface CompletedToolCall extends PendingToolCall {
19
+ result: MCPContent[];
20
+ isError?: boolean;
21
+ uiResource?: UIResource;
22
+ autoApproveReason?: AutoApproveReason;
23
+ }
24
+
25
+ export interface PendingToolConfirmation {
26
+ confirmationId: string;
27
+ serverId: string;
28
+ toolName: string;
29
+ toolArgs: unknown;
30
+ }
31
+
32
+ export type ConfirmationScope = 'once' | 'thread' | 'always';
33
+
34
+ interface ChatState {
35
+ threads: ThreadSummary[];
36
+ currentThread: Thread | null;
37
+ isLoadingThreads: boolean;
38
+ isStreaming: boolean;
39
+ streamingContent: string;
40
+ pendingToolCalls: PendingToolCall[];
41
+ completedToolCalls: CompletedToolCall[];
42
+ pendingConfirmation: PendingToolConfirmation | null;
43
+
44
+ // Team context
45
+ currentTeamId: string | null;
46
+
47
+ // Project context
48
+ currentProjectId: string | null;
49
+
50
+ // Agent selection
51
+ availableAgents: AgentInfo[];
52
+ selectedAgentId: string | null;
53
+ isLoadingAgents: boolean;
54
+
55
+ // Actions
56
+ setCurrentTeamId: (teamId: string | null) => void;
57
+ setCurrentProjectId: (projectId: string | null) => void;
58
+ loadThreads: (teamId?: string | null) => Promise<void>;
59
+ loadThread: (threadId: string) => Promise<void>;
60
+ createThread: (agentId?: string, teamId?: string | null, projectId?: string | null) => Promise<Thread>;
61
+ deleteThread: (threadId: string) => Promise<void>;
62
+ renameThread: (threadId: string, title: string) => Promise<void>;
63
+ sendMessage: (content: string, files?: File[], agentId?: string, teamId?: string | null, projectId?: string | null) => Promise<void>;
64
+ regenerateMessage: (messageId: string) => Promise<void>;
65
+ branchFromMessage: (messageId: string) => Promise<Thread>;
66
+ clearCurrentThread: () => void;
67
+ confirmTool: (confirmationId: string, approved: boolean, scope?: ConfirmationScope) => Promise<void>;
68
+ loadAgents: () => Promise<void>;
69
+ setSelectedAgentId: (agentId: string | null) => void;
70
+ }
71
+
72
+ export const useChatStore = create<ChatState>((set, get) => ({
73
+ threads: [],
74
+ currentThread: null,
75
+ isLoadingThreads: false,
76
+ isStreaming: false,
77
+ streamingContent: '',
78
+ pendingToolCalls: [],
79
+ completedToolCalls: [],
80
+ pendingConfirmation: null,
81
+
82
+ // Team context
83
+ currentTeamId: null,
84
+
85
+ // Project context
86
+ currentProjectId: null,
87
+
88
+ // Agent selection state
89
+ availableAgents: [],
90
+ selectedAgentId: null,
91
+ isLoadingAgents: false,
92
+
93
+ setCurrentTeamId: (teamId: string | null) => {
94
+ set({ currentTeamId: teamId, currentProjectId: null, threads: [], currentThread: null });
95
+ },
96
+
97
+ setCurrentProjectId: (projectId: string | null) => {
98
+ set({ currentProjectId: projectId });
99
+ },
100
+
101
+ loadThreads: async (teamId?: string | null) => {
102
+ set({ isLoadingThreads: true });
103
+ try {
104
+ // Use provided teamId, or fall back to current teamId in store
105
+ const effectiveTeamId = teamId !== undefined ? teamId : get().currentTeamId;
106
+ const url = effectiveTeamId
107
+ ? `/api/threads?teamId=${effectiveTeamId}`
108
+ : '/api/threads';
109
+ const response = await api.get<{ threads: ThreadSummary[] }>(url);
110
+ set({ threads: response.threads });
111
+ } finally {
112
+ set({ isLoadingThreads: false });
113
+ }
114
+ },
115
+
116
+ loadThread: async (threadId: string) => {
117
+ const response = await api.get<{ thread: Thread }>(`/api/threads/${threadId}`);
118
+ set({ currentThread: response.thread });
119
+ },
120
+
121
+ createThread: async (agentId?: string, teamId?: string | null, projectId?: string | null) => {
122
+ // Use provided teamId or fall back to current teamId in store
123
+ const effectiveTeamId = teamId !== undefined ? teamId : get().currentTeamId;
124
+ // Use provided projectId or fall back to current projectId in store
125
+ const effectiveProjectId = projectId !== undefined ? projectId : get().currentProjectId;
126
+ const response = await api.post<{ thread: Thread }>('/api/threads', {
127
+ agentId,
128
+ teamId: effectiveTeamId || undefined,
129
+ projectId: effectiveProjectId || undefined,
130
+ });
131
+ set((state) => ({
132
+ threads: [
133
+ {
134
+ id: response.thread.id,
135
+ title: response.thread.title,
136
+ createdAt: response.thread.createdAt,
137
+ updatedAt: response.thread.updatedAt,
138
+ messageCount: 0,
139
+ agentId: response.thread.agentId,
140
+ agentName: response.thread.agentName,
141
+ projectId: response.thread.projectId,
142
+ },
143
+ ...state.threads,
144
+ ],
145
+ currentThread: { ...response.thread, messages: [] },
146
+ selectedAgentId: null, // Reset selection after creating thread
147
+ }));
148
+ return response.thread;
149
+ },
150
+
151
+ deleteThread: async (threadId: string) => {
152
+ await api.delete(`/api/threads/${threadId}`);
153
+ set((state) => ({
154
+ threads: state.threads.filter((t) => t.id !== threadId),
155
+ currentThread: state.currentThread?.id === threadId ? null : state.currentThread,
156
+ }));
157
+ },
158
+
159
+ renameThread: async (threadId: string, title: string) => {
160
+ await api.patch(`/api/threads/${threadId}`, { title });
161
+ set((state) => ({
162
+ threads: state.threads.map((t) =>
163
+ t.id === threadId ? { ...t, title } : t
164
+ ),
165
+ currentThread:
166
+ state.currentThread?.id === threadId
167
+ ? { ...state.currentThread, title }
168
+ : state.currentThread,
169
+ }));
170
+ },
171
+
172
+ sendMessage: async (content: string, files?: File[], agentId?: string, teamId?: string | null, projectId?: string | null) => {
173
+ const state = get();
174
+ const { currentThread, selectedAgentId, currentTeamId, currentProjectId } = state;
175
+ // Use provided agentId or selected agent for new threads
176
+ const effectiveAgentId = agentId || (!currentThread ? selectedAgentId : null);
177
+ // Use provided teamId or fall back to current teamId in store
178
+ const effectiveTeamId = teamId !== undefined ? teamId : currentTeamId;
179
+ // Use provided projectId or fall back to current projectId in store
180
+ const effectiveProjectId = projectId !== undefined ? projectId : currentProjectId;
181
+ console.log('[Chat] sendMessage called:', {
182
+ paramAgentId: agentId,
183
+ currentThreadId: currentThread?.id,
184
+ currentThreadAgentId: currentThread?.agentId,
185
+ storeSelectedAgentId: selectedAgentId,
186
+ effectiveAgentId,
187
+ hasCurrentThread: !!currentThread,
188
+ });
189
+ // Debug: Log existing messages before adding new one
190
+ if (currentThread?.messages) {
191
+ console.log('[Chat] Existing messages before send:', currentThread.messages.length);
192
+ currentThread.messages.forEach((msg, i) => {
193
+ if (msg.toolResults?.length) {
194
+ console.log(`[Chat] Existing msg ${i}: role=${msg.role}, toolResults=${msg.toolResults.length}, uiResources=${msg.toolResults.filter(tr => tr.uiResource?.text).length}`);
195
+ }
196
+ });
197
+ }
198
+
199
+ // Upload files as documents and build mentions
200
+ let contentWithMentions = content;
201
+ if (files && files.length > 0) {
202
+ const uploadedPaths: string[] = [];
203
+
204
+ for (const file of files) {
205
+ const formData = new FormData();
206
+ formData.append('file', file);
207
+ // Add team/project context if available
208
+ if (effectiveTeamId) formData.append('teamId', effectiveTeamId);
209
+ if (effectiveProjectId) formData.append('projectId', effectiveProjectId);
210
+
211
+ const uploadResponse = await fetch('/api/documents/upload', {
212
+ method: 'POST',
213
+ body: formData,
214
+ credentials: 'include',
215
+ });
216
+
217
+ if (!uploadResponse.ok) {
218
+ const errorData = await uploadResponse.json().catch(() => ({}));
219
+ throw new Error(errorData.error?.message || 'File upload failed');
220
+ }
221
+
222
+ const uploadData = await uploadResponse.json();
223
+ if (uploadData.document?.path) {
224
+ uploadedPaths.push(uploadData.document.path);
225
+ }
226
+ }
227
+
228
+ // Append @mentions for uploaded documents to the message
229
+ if (uploadedPaths.length > 0) {
230
+ const mentions = uploadedPaths.join(' ');
231
+ contentWithMentions = content ? `${content}\n\n${mentions}` : mentions;
232
+ }
233
+ }
234
+
235
+ // Add optimistic user message
236
+ const userMessage: Message = {
237
+ id: `temp-${Date.now()}`,
238
+ threadId: currentThread?.id || 'pending',
239
+ role: 'user',
240
+ content: contentWithMentions,
241
+ createdAt: new Date(),
242
+ };
243
+
244
+ // If no current thread, create a temporary one to show the message immediately
245
+ set((state) => ({
246
+ currentThread: state.currentThread
247
+ ? {
248
+ ...state.currentThread,
249
+ messages: [...state.currentThread.messages, userMessage],
250
+ }
251
+ : {
252
+ id: 'pending',
253
+ title: content.slice(0, 50) + (content.length > 50 ? '...' : ''),
254
+ userId: undefined,
255
+ createdAt: new Date(),
256
+ updatedAt: new Date(),
257
+ messages: [userMessage],
258
+ },
259
+ isStreaming: true,
260
+ streamingContent: '',
261
+ pendingToolCalls: [],
262
+ completedToolCalls: [],
263
+ }));
264
+
265
+ try {
266
+ const response = await fetch('/api/chat', {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({
270
+ threadId: currentThread?.id,
271
+ content: contentWithMentions,
272
+ agentId: effectiveAgentId || undefined,
273
+ teamId: !currentThread?.id ? effectiveTeamId || undefined : undefined,
274
+ projectId: !currentThread?.id ? effectiveProjectId || undefined : undefined,
275
+ }),
276
+ credentials: 'include',
277
+ });
278
+
279
+ console.log('[Chat] Response status:', response.status);
280
+
281
+ if (!response.ok) {
282
+ const errorText = await response.text();
283
+ console.error('[Chat] Request failed:', response.status, errorText);
284
+ throw new Error(`Chat request failed: ${response.status}`);
285
+ }
286
+
287
+ const reader = response.body?.getReader();
288
+ const decoder = new TextDecoder();
289
+
290
+ if (!reader) {
291
+ throw new Error('No response body');
292
+ }
293
+
294
+ let fullContent = '';
295
+ let threadId = currentThread?.id;
296
+ let messageId = '';
297
+
298
+ while (true) {
299
+ const { done, value } = await reader.read();
300
+ if (done) break;
301
+
302
+ const text = decoder.decode(value);
303
+ const lines = text.split('\n');
304
+
305
+ for (const line of lines) {
306
+ if (line.startsWith('data: ')) {
307
+ try {
308
+ const rawData = line.slice(6);
309
+ // Debug: log all SSE events
310
+ if (rawData.includes('tool_use') || rawData.includes('tool_result')) {
311
+ console.log('[Chat] SSE raw event:', rawData.slice(0, 200) + (rawData.length > 200 ? '...' : ''));
312
+ }
313
+ const data = JSON.parse(rawData) as {
314
+ type: string;
315
+ content?: string;
316
+ threadId?: string;
317
+ title?: string;
318
+ messageId?: string;
319
+ error?: string;
320
+ // Tool event fields
321
+ id?: string;
322
+ name?: string;
323
+ serverId?: string;
324
+ input?: Record<string, unknown>;
325
+ isError?: boolean;
326
+ uiResource?: UIResource;
327
+ // Tool confirmation fields
328
+ confirmationId?: string;
329
+ toolName?: string;
330
+ toolArgs?: unknown;
331
+ approved?: boolean;
332
+ autoApproveReason?: AutoApproveReason;
333
+ toolCalls?: Array<{
334
+ id: string;
335
+ name: string;
336
+ serverId: string;
337
+ input: Record<string, unknown>;
338
+ result: MCPContent[];
339
+ isError?: boolean;
340
+ }>;
341
+ };
342
+
343
+ if (data.type === 'thread' && data.threadId) {
344
+ threadId = data.threadId;
345
+ const newTitle = data.title;
346
+ // Update thread ID, title, and message threadIds
347
+ set((state) => ({
348
+ currentThread: state.currentThread
349
+ ? {
350
+ ...state.currentThread,
351
+ id: threadId!,
352
+ title: newTitle || state.currentThread.title,
353
+ messages: state.currentThread.messages.map((m) => ({
354
+ ...m,
355
+ threadId: threadId!,
356
+ })),
357
+ }
358
+ : null,
359
+ // Also update the thread title in the sidebar list
360
+ threads: newTitle
361
+ ? state.threads.map((t) =>
362
+ t.id === threadId ? { ...t, title: newTitle } : t
363
+ )
364
+ : state.threads,
365
+ }));
366
+ } else if (data.type === 'start') {
367
+ // Stream starting - good for debugging
368
+ console.log('[Chat] Stream started');
369
+ } else if (data.type === 'delta' && data.content) {
370
+ fullContent += data.content;
371
+ set({ streamingContent: fullContent });
372
+ } else if (data.type === 'tool_use' && data.id && data.name && data.serverId) {
373
+ // Add pending tool call
374
+ console.log('[Chat] Tool use:', data.name);
375
+ set((state) => ({
376
+ pendingToolCalls: [
377
+ ...state.pendingToolCalls,
378
+ {
379
+ id: data.id!,
380
+ name: data.name!,
381
+ serverId: data.serverId!,
382
+ input: data.input || {},
383
+ },
384
+ ],
385
+ }));
386
+ } else if (data.type === 'tool_result' && data.id) {
387
+ // Move from pending to completed (or create new if no pending)
388
+ console.log('[Chat] Tool result for:', data.id, 'name:', data.name);
389
+ console.log('[Chat] Tool result uiResource:', data.uiResource ? { hasText: !!data.uiResource.text, textLen: data.uiResource.text?.length, mimeType: data.uiResource.mimeType } : 'none');
390
+ set((state) => {
391
+ const pendingCall = state.pendingToolCalls.find((tc) => tc.id === data.id);
392
+
393
+ // Create the completed tool call using pendingCall if found, otherwise use data from event
394
+ const completedCall = pendingCall
395
+ ? {
396
+ ...pendingCall,
397
+ result: (data.content as unknown as MCPContent[]) || [],
398
+ isError: data.isError,
399
+ uiResource: data.uiResource,
400
+ }
401
+ : {
402
+ // Fallback: create from tool_result event data (server now includes name/serverId/input)
403
+ id: data.id!,
404
+ name: data.name || 'unknown',
405
+ serverId: data.serverId || 'unknown',
406
+ input: data.input || {},
407
+ result: (data.content as unknown as MCPContent[]) || [],
408
+ isError: data.isError,
409
+ uiResource: data.uiResource,
410
+ };
411
+
412
+ return {
413
+ pendingToolCalls: state.pendingToolCalls.filter((tc) => tc.id !== data.id),
414
+ completedToolCalls: [
415
+ ...state.completedToolCalls,
416
+ completedCall,
417
+ ],
418
+ };
419
+ });
420
+ // Verify the update
421
+ const { completedToolCalls: updatedCalls } = get();
422
+ console.log('[Chat] After tool_result, completedToolCalls:', updatedCalls.length, updatedCalls.map(tc => ({ name: tc.name, hasUiResource: !!tc.uiResource, textLen: tc.uiResource?.text?.length })));
423
+ } else if (data.type === 'tool_pending_confirmation') {
424
+ // Show confirmation modal
425
+ console.log('[Chat] Tool pending confirmation:', data.confirmationId, data.toolName);
426
+ set({
427
+ pendingConfirmation: {
428
+ confirmationId: data.confirmationId!,
429
+ serverId: data.serverId!,
430
+ toolName: data.toolName!,
431
+ toolArgs: data.toolArgs,
432
+ },
433
+ });
434
+ } else if (data.type === 'tool_confirmed') {
435
+ // Clear pending confirmation
436
+ console.log('[Chat] Tool confirmed:', data.confirmationId, data.approved);
437
+ set({ pendingConfirmation: null });
438
+ } else if (data.type === 'tool_auto_approved') {
439
+ // Track auto-approved tools for UI indicator
440
+ console.log('[Chat] Tool auto-approved:', data.toolName, data.autoApproveReason);
441
+ // Find the pending tool call and update it with auto-approve info
442
+ set((state) => ({
443
+ pendingToolCalls: state.pendingToolCalls.map((tc) =>
444
+ tc.id === data.id
445
+ ? { ...tc }
446
+ : tc
447
+ ),
448
+ }));
449
+ } else if (data.type === 'done') {
450
+ messageId = data.messageId || '';
451
+ // Note: We don't overwrite completedToolCalls here because
452
+ // the tool_result events already include uiResource which
453
+ // the done event's toolCalls don't have
454
+ console.log('[Chat] Stream done, messageId:', messageId);
455
+ } else if (data.type === 'error') {
456
+ console.error('[Chat] Stream error:', data.error);
457
+ throw new Error(data.error || 'Unknown error');
458
+ }
459
+ } catch (parseError) {
460
+ // Skip invalid JSON but log unexpected errors
461
+ if (parseError instanceof Error && parseError.message !== 'Unknown error') {
462
+ console.warn('[Chat] Parse error:', parseError);
463
+ }
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ // Finalize the assistant message with tool calls
470
+ const { completedToolCalls } = get();
471
+ console.log('[Chat] Finalizing message with completedToolCalls:', completedToolCalls.length);
472
+ completedToolCalls.forEach((tc, i) => {
473
+ console.log(`[Chat] Tool ${i}: ${tc.name}, hasUiResource: ${!!tc.uiResource}, uiResourceText: ${tc.uiResource?.text?.length || 0}`);
474
+ });
475
+ const assistantMessage: Message = {
476
+ id: messageId,
477
+ threadId: threadId || '',
478
+ role: 'assistant',
479
+ content: fullContent,
480
+ toolCalls: completedToolCalls.length > 0
481
+ ? completedToolCalls.map((tc) => ({
482
+ id: tc.id,
483
+ serverId: tc.serverId,
484
+ toolName: tc.name,
485
+ arguments: tc.input,
486
+ status: tc.isError ? 'error' as const : 'completed' as const,
487
+ }))
488
+ : undefined,
489
+ toolResults: completedToolCalls.length > 0
490
+ ? completedToolCalls.map((tc) => ({
491
+ toolCallId: tc.id,
492
+ content: tc.result,
493
+ isError: tc.isError,
494
+ uiResource: tc.uiResource,
495
+ }))
496
+ : undefined,
497
+ createdAt: new Date(),
498
+ };
499
+ console.log('[Chat] Finalized message toolResults:', assistantMessage.toolResults?.length || 0, assistantMessage.toolResults?.map(tr => ({ hasUiResource: !!tr.uiResource, textLen: tr.uiResource?.text?.length })));
500
+
501
+ set((state) => ({
502
+ currentThread: state.currentThread
503
+ ? {
504
+ ...state.currentThread,
505
+ id: threadId || state.currentThread.id,
506
+ messages: [...state.currentThread.messages, assistantMessage],
507
+ }
508
+ : null,
509
+ isStreaming: false,
510
+ streamingContent: '',
511
+ pendingToolCalls: [],
512
+ completedToolCalls: [],
513
+ pendingConfirmation: null,
514
+ }));
515
+
516
+ // Refresh thread list to update previews
517
+ get().loadThreads();
518
+ } catch (error) {
519
+ set({ isStreaming: false, streamingContent: '', pendingConfirmation: null });
520
+ throw error;
521
+ }
522
+ },
523
+
524
+ regenerateMessage: async (messageId: string) => {
525
+ set({ isStreaming: true, streamingContent: '' });
526
+
527
+ try {
528
+ const response = await fetch(`/api/chat/regenerate/${messageId}`, {
529
+ method: 'POST',
530
+ credentials: 'include',
531
+ });
532
+
533
+ if (!response.ok) {
534
+ throw new Error('Regenerate request failed');
535
+ }
536
+
537
+ const reader = response.body?.getReader();
538
+ const decoder = new TextDecoder();
539
+
540
+ if (!reader) {
541
+ throw new Error('No response body');
542
+ }
543
+
544
+ let fullContent = '';
545
+
546
+ while (true) {
547
+ const { done, value } = await reader.read();
548
+ if (done) break;
549
+
550
+ const text = decoder.decode(value);
551
+ const lines = text.split('\n');
552
+
553
+ for (const line of lines) {
554
+ if (line.startsWith('data: ')) {
555
+ try {
556
+ const data: StreamingMessage = JSON.parse(line.slice(6));
557
+
558
+ if (data.type === 'delta' && data.content) {
559
+ fullContent += data.content;
560
+ set({ streamingContent: fullContent });
561
+ } else if (data.type === 'error') {
562
+ throw new Error(data.error || 'Unknown error');
563
+ }
564
+ } catch {
565
+ // Skip invalid JSON
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ // Update the message
572
+ set((state) => ({
573
+ currentThread: state.currentThread
574
+ ? {
575
+ ...state.currentThread,
576
+ messages: state.currentThread.messages.map((m) =>
577
+ m.id === messageId ? { ...m, content: fullContent } : m
578
+ ),
579
+ }
580
+ : null,
581
+ isStreaming: false,
582
+ streamingContent: '',
583
+ }));
584
+ } catch (error) {
585
+ set({ isStreaming: false, streamingContent: '' });
586
+ throw error;
587
+ }
588
+ },
589
+
590
+ branchFromMessage: async (messageId: string) => {
591
+ const response = await api.post<{ thread: Thread }>(`/api/chat/branch/${messageId}`, {});
592
+
593
+ const newThread = response.thread;
594
+
595
+ // Add the new thread to the list
596
+ set((state) => ({
597
+ threads: [
598
+ {
599
+ id: newThread.id,
600
+ title: newThread.title,
601
+ createdAt: newThread.createdAt,
602
+ updatedAt: newThread.updatedAt,
603
+ messageCount: newThread.messages?.length || 0,
604
+ agentId: newThread.agentId,
605
+ agentName: newThread.agentName,
606
+ projectId: newThread.projectId,
607
+ parentThreadId: newThread.parentThreadId,
608
+ },
609
+ ...state.threads,
610
+ ],
611
+ currentThread: newThread,
612
+ }));
613
+
614
+ return newThread;
615
+ },
616
+
617
+ clearCurrentThread: () => {
618
+ set({ currentThread: null });
619
+ },
620
+
621
+ confirmTool: async (confirmationId: string, approved: boolean, scope?: ConfirmationScope) => {
622
+ try {
623
+ console.log('[Chat] Confirming tool:', confirmationId, approved, scope);
624
+ const response = await fetch('/api/chat/confirm-tool', {
625
+ method: 'POST',
626
+ headers: { 'Content-Type': 'application/json' },
627
+ body: JSON.stringify({ confirmationId, approved, scope }),
628
+ credentials: 'include',
629
+ });
630
+
631
+ if (!response.ok) {
632
+ const errorText = await response.text();
633
+ console.error('[Chat] Confirm tool failed:', response.status, errorText);
634
+ throw new Error(`Confirm tool failed: ${response.status}`);
635
+ }
636
+
637
+ console.log('[Chat] Tool confirmation sent successfully');
638
+ } catch (error) {
639
+ console.error('[Chat] Error confirming tool:', error);
640
+ // Clear pending confirmation on error to prevent UI from being stuck
641
+ set({ pendingConfirmation: null });
642
+ throw error;
643
+ }
644
+ },
645
+
646
+ loadAgents: async () => {
647
+ set({ isLoadingAgents: true });
648
+ try {
649
+ const response = await api.get<{ agents: AgentInfo[] }>('/api/agents');
650
+ const agents = response.agents;
651
+
652
+ // Set default agent as selected if none is selected
653
+ const defaultAgent = agents.find((a) => a.isDefault) || agents[0];
654
+
655
+ set({
656
+ availableAgents: agents,
657
+ selectedAgentId: get().selectedAgentId || defaultAgent?.id || null,
658
+ });
659
+ } catch (error) {
660
+ console.error('[Chat] Failed to load agents:', error);
661
+ } finally {
662
+ set({ isLoadingAgents: false });
663
+ }
664
+ },
665
+
666
+ setSelectedAgentId: (agentId: string | null) => {
667
+ console.log('[Chat] setSelectedAgentId called with:', agentId);
668
+ set({ selectedAgentId: agentId });
669
+ },
670
+ }));