@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,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
|
+
}));
|