@agentforge-ai/cli 0.4.3 → 0.5.1

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 (67) hide show
  1. package/dist/default/convex/agents.ts +204 -0
  2. package/dist/default/convex/apiKeys.ts +133 -0
  3. package/dist/default/convex/cronJobs.ts +224 -0
  4. package/dist/default/convex/files.ts +103 -0
  5. package/dist/default/convex/folders.ts +110 -0
  6. package/dist/default/convex/heartbeat.ts +371 -0
  7. package/dist/default/convex/logs.ts +66 -0
  8. package/dist/default/convex/mastraIntegration.ts +185 -0
  9. package/dist/default/convex/mcpConnections.ts +127 -0
  10. package/dist/default/convex/messages.ts +90 -0
  11. package/dist/default/convex/projects.ts +114 -0
  12. package/dist/default/convex/schema.ts +150 -83
  13. package/dist/default/convex/sessions.ts +174 -0
  14. package/dist/default/convex/settings.ts +79 -0
  15. package/dist/default/convex/skills.ts +178 -0
  16. package/dist/default/convex/threads.ts +100 -0
  17. package/dist/default/convex/usage.ts +195 -0
  18. package/dist/default/convex/vault.ts +397 -0
  19. package/dist/default/dashboard/app/main.tsx +7 -3
  20. package/dist/default/dashboard/app/routes/agents.tsx +103 -161
  21. package/dist/default/dashboard/app/routes/chat.tsx +163 -317
  22. package/dist/default/dashboard/app/routes/connections.tsx +247 -386
  23. package/dist/default/dashboard/app/routes/cron.tsx +127 -286
  24. package/dist/default/dashboard/app/routes/files.tsx +184 -167
  25. package/dist/default/dashboard/app/routes/index.tsx +63 -96
  26. package/dist/default/dashboard/app/routes/projects.tsx +106 -225
  27. package/dist/default/dashboard/app/routes/sessions.tsx +87 -253
  28. package/dist/default/dashboard/app/routes/settings.tsx +316 -532
  29. package/dist/default/dashboard/app/routes/skills.tsx +329 -216
  30. package/dist/default/dashboard/app/routes/usage.tsx +107 -150
  31. package/dist/default/dashboard/tsconfig.json +3 -2
  32. package/dist/default/dashboard/vite.config.ts +6 -0
  33. package/dist/index.js +256 -49
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/default/convex/agents.ts +204 -0
  37. package/templates/default/convex/apiKeys.ts +133 -0
  38. package/templates/default/convex/cronJobs.ts +224 -0
  39. package/templates/default/convex/files.ts +103 -0
  40. package/templates/default/convex/folders.ts +110 -0
  41. package/templates/default/convex/heartbeat.ts +371 -0
  42. package/templates/default/convex/logs.ts +66 -0
  43. package/templates/default/convex/mastraIntegration.ts +185 -0
  44. package/templates/default/convex/mcpConnections.ts +127 -0
  45. package/templates/default/convex/messages.ts +90 -0
  46. package/templates/default/convex/projects.ts +114 -0
  47. package/templates/default/convex/schema.ts +150 -83
  48. package/templates/default/convex/sessions.ts +174 -0
  49. package/templates/default/convex/settings.ts +79 -0
  50. package/templates/default/convex/skills.ts +178 -0
  51. package/templates/default/convex/threads.ts +100 -0
  52. package/templates/default/convex/usage.ts +195 -0
  53. package/templates/default/convex/vault.ts +397 -0
  54. package/templates/default/dashboard/app/main.tsx +7 -3
  55. package/templates/default/dashboard/app/routes/agents.tsx +103 -161
  56. package/templates/default/dashboard/app/routes/chat.tsx +163 -317
  57. package/templates/default/dashboard/app/routes/connections.tsx +247 -386
  58. package/templates/default/dashboard/app/routes/cron.tsx +127 -286
  59. package/templates/default/dashboard/app/routes/files.tsx +184 -167
  60. package/templates/default/dashboard/app/routes/index.tsx +63 -96
  61. package/templates/default/dashboard/app/routes/projects.tsx +106 -225
  62. package/templates/default/dashboard/app/routes/sessions.tsx +87 -253
  63. package/templates/default/dashboard/app/routes/settings.tsx +316 -532
  64. package/templates/default/dashboard/app/routes/skills.tsx +329 -216
  65. package/templates/default/dashboard/app/routes/usage.tsx +107 -150
  66. package/templates/default/dashboard/tsconfig.json +3 -2
  67. package/templates/default/dashboard/vite.config.ts +6 -0
@@ -1,10 +1,12 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
2
  import { DashboardLayout } from '../components/DashboardLayout';
3
3
  import { useState, useEffect, useRef, useCallback } from 'react';
4
- import { Send, Plus, Bot, User, Shield, ShieldAlert, Lock, AlertTriangle, Paperclip, Mic, Settings2 } from 'lucide-react';
4
+ import { useQuery, useMutation } from 'convex/react';
5
+ import { api } from '@convex/_generated/api';
6
+ import { Send, Plus, Bot, User, Shield, ShieldAlert, Lock, AlertTriangle, Paperclip, Mic, Settings2, X, MessageSquare } from 'lucide-react';
5
7
 
6
8
  // ============================================================
7
- // SECRET DETECTION ENGINE (client-side mirror of vault patterns)
9
+ // SECRET DETECTION ENGINE
8
10
  // ============================================================
9
11
  const SECRET_PATTERNS = [
10
12
  { pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openai", name: "OpenAI API Key" },
@@ -13,23 +15,12 @@ const SECRET_PATTERNS = [
13
15
  { pattern: /AIza[a-zA-Z0-9_-]{35}/g, category: "api_key", provider: "google", name: "Google API Key" },
14
16
  { pattern: /xai-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "xai", name: "xAI API Key" },
15
17
  { pattern: /ghp_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub Token" },
16
- { pattern: /gho_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub OAuth" },
17
- { pattern: /glpat-[a-zA-Z0-9_-]{20,}/g, category: "token", provider: "gitlab", name: "GitLab Token" },
18
- { pattern: /xoxb-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack Bot Token" },
19
- { pattern: /xoxp-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack User Token" },
20
18
  { pattern: /AKIA[A-Z0-9]{16}/g, category: "credential", provider: "aws", name: "AWS Access Key" },
21
19
  { pattern: /sk_live_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Live Key" },
22
20
  { pattern: /sk_test_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Test Key" },
23
- { pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, category: "api_key", provider: "sendgrid", name: "SendGrid Key" },
24
- { pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/g, category: "token", provider: "jwt", name: "JWT Token" },
25
21
  ];
26
22
 
27
- interface DetectedSecret {
28
- match: string;
29
- category: string;
30
- provider: string;
31
- name: string;
32
- }
23
+ interface DetectedSecret { match: string; category: string; provider: string; name: string; }
33
24
 
34
25
  function detectSecrets(text: string): DetectedSecret[] {
35
26
  const detected: DetectedSecret[] = [];
@@ -51,159 +42,87 @@ function maskSecret(value: string): string {
51
42
  function censorText(text: string, secrets: DetectedSecret[]): string {
52
43
  let censored = text;
53
44
  for (const secret of secrets) {
54
- censored = censored.replace(secret.match, `[🔒 ${secret.name}: ${maskSecret(secret.match)}]`);
45
+ censored = censored.replace(secret.match, `[REDACTED: ${secret.name}]`);
55
46
  }
56
47
  return censored;
57
48
  }
58
49
 
59
- // ============================================================
60
- // Mock data (replace with Convex queries)
61
- // ============================================================
62
- const mockSessions = [
63
- { id: 'ses_1', name: 'Main Session' },
64
- { id: 'ses_2', name: 'Customer Support' },
65
- { id: 'ses_3', name: 'Code Review' },
66
- ];
67
-
68
- const mockAgents = [
69
- { id: 'agent_1', name: 'GPT-4.1 Mini', provider: 'openai' },
70
- { id: 'agent_2', name: 'Claude 3.5 Sonnet', provider: 'anthropic' },
71
- { id: 'agent_3', name: 'Grok 2', provider: 'xai' },
72
- { id: 'agent_4', name: 'Gemini Pro', provider: 'google' },
73
- ];
74
-
75
- interface ChatMessage {
76
- id: string;
77
- text: string;
78
- sender: 'user' | 'assistant' | 'system';
79
- timestamp: string;
80
- isRedacted?: boolean;
81
- secretsCaptured?: Array<{ name: string; masked: string }>;
82
- }
83
-
84
- // ============================================================
85
- // Chat Page Component
86
- // ============================================================
87
50
  export const Route = createFileRoute('/chat')({ component: ChatPageComponent });
88
51
 
89
52
  function ChatPageComponent() {
90
- const [sessions] = useState(mockSessions);
91
- const [agents] = useState(mockAgents);
92
- const [currentSessionId, setCurrentSessionId] = useState(mockSessions[0]?.id || null);
93
- const [messages, setMessages] = useState<ChatMessage[]>([]);
53
+ const agents = useQuery(api.agents.list, {}) ?? [];
54
+ const threads = useQuery(api.threads.list, {}) ?? [];
55
+ const createThread = useMutation(api.threads.create);
56
+ const addMessage = useMutation(api.messages.add);
57
+ const storeSecret = useMutation(api.vault.store);
58
+
59
+ const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
60
+ const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
94
61
  const [input, setInput] = useState('');
95
62
  const [isTyping, setIsTyping] = useState(false);
96
- const [currentAgentId, setCurrentAgentId] = useState(mockAgents[0]?.id || null);
97
63
  const [secretWarning, setSecretWarning] = useState<DetectedSecret[] | null>(null);
98
64
  const [pendingMessage, setPendingMessage] = useState<string | null>(null);
99
- const [vaultNotification, setVaultNotification] = useState<string | null>(null);
100
-
101
65
  const messagesEndRef = useRef<HTMLDivElement>(null);
102
66
  const inputRef = useRef<HTMLInputElement>(null);
103
67
 
104
- const scrollToBottom = () => {
105
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
106
- };
107
-
108
- useEffect(() => { scrollToBottom(); }, [messages, isTyping]);
68
+ // Load messages for current thread
69
+ const messages = useQuery(api.messages.list, currentThreadId ? { threadId: currentThreadId as any } : "skip") ?? [];
109
70
 
71
+ // Auto-select first agent
110
72
  useEffect(() => {
111
- if (currentSessionId) {
112
- setMessages([]);
113
- setIsTyping(true);
114
- setTimeout(() => {
115
- const agent = agents.find(a => a.id === currentAgentId);
116
- setMessages([{
117
- id: 'msg_welcome',
118
- text: `Hello! I'm ${agent?.name || 'your agent'}. How can I help you today?`,
119
- sender: 'assistant',
120
- timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
121
- }]);
122
- setIsTyping(false);
123
- }, 500);
73
+ if (agents.length > 0 && !currentAgentId) {
74
+ setCurrentAgentId(agents[0].id);
124
75
  }
125
- }, [currentSessionId]);
76
+ }, [agents, currentAgentId]);
126
77
 
127
- // Real-time secret detection as user types
128
- const inputHasSecrets = input.length > 8 ? detectSecrets(input) : [];
78
+ // Auto-scroll
79
+ useEffect(() => {
80
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
81
+ }, [messages]);
129
82
 
130
- const handleSendMessage = useCallback((e: React.FormEvent) => {
131
- e.preventDefault();
132
- if (!input.trim() || !currentSessionId) return;
83
+ const inputHasSecrets = input ? detectSecrets(input) : [];
133
84
 
134
- const detectedSecrets = detectSecrets(input);
85
+ const handleSendMessage = async (e: React.FormEvent) => {
86
+ e.preventDefault();
87
+ if (!input.trim()) return;
135
88
 
136
- if (detectedSecrets.length > 0) {
137
- // Show warning before sending
138
- setSecretWarning(detectedSecrets);
89
+ const secrets = detectSecrets(input);
90
+ if (secrets.length > 0) {
91
+ setSecretWarning(secrets);
139
92
  setPendingMessage(input);
140
93
  return;
141
94
  }
142
95
 
143
- // No secrets detected, send normally
144
- sendMessageToChat(input);
145
- }, [input, currentSessionId, currentAgentId]);
146
-
147
- const sendMessageToChat = (text: string, secrets?: DetectedSecret[]) => {
148
- let displayText = text;
149
- let secretsCaptured: Array<{ name: string; masked: string }> = [];
150
- let isRedacted = false;
151
-
152
- if (secrets && secrets.length > 0) {
153
- // Censor the message and auto-store secrets
154
- displayText = censorText(text, secrets);
155
- isRedacted = true;
156
- secretsCaptured = secrets.map(s => ({ name: s.name, masked: maskSecret(s.match) }));
157
-
158
- // In production: call vault.censorMessage mutation here
159
- // const result = await censorMessage({ text, userId, autoStore: true });
96
+ await sendMessage(input);
97
+ };
160
98
 
161
- // Show vault notification
162
- setVaultNotification(
163
- `${secrets.length} secret${secrets.length > 1 ? 's' : ''} detected and securely stored in the Vault.`
164
- );
165
- setTimeout(() => setVaultNotification(null), 5000);
99
+ const sendMessage = async (text: string) => {
100
+ let threadId = currentThreadId;
101
+ if (!threadId && currentAgentId) {
102
+ threadId = await createThread({ agentId: currentAgentId, name: `Chat ${new Date().toLocaleString()}` }) as any;
103
+ setCurrentThreadId(threadId);
166
104
  }
105
+ if (!threadId) return;
167
106
 
168
- const userMessage: ChatMessage = {
169
- id: `msg_${Date.now()}`,
170
- text: displayText,
171
- sender: 'user',
172
- timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
173
- isRedacted,
174
- secretsCaptured: secretsCaptured.length > 0 ? secretsCaptured : undefined,
175
- };
176
-
177
- setMessages(prev => [...prev, userMessage]);
107
+ await addMessage({ threadId: threadId as any, role: 'user', content: text });
178
108
  setInput('');
179
109
  setIsTyping(true);
180
110
 
181
- // Simulate agent response
182
- setTimeout(() => {
183
- const agent = agents.find(a => a.id === currentAgentId);
184
- let responseText: string;
185
-
186
- if (isRedacted) {
187
- responseText = `I noticed you shared ${secretsCaptured.length} secret${secretsCaptured.length > 1 ? 's' : ''}. They've been automatically encrypted and stored in your Secure Vault. The original values are never stored in chat history.\n\nHow else can I help you?`;
188
- } else {
189
- responseText = `I understand your request. Let me work on that for you.\n\n*This is a simulated response from ${agent?.name || 'the agent'}.*`;
190
- }
191
-
192
- const assistantResponse: ChatMessage = {
193
- id: `msg_${Date.now() + 1}`,
194
- text: responseText,
195
- sender: 'assistant',
196
- timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
197
- };
198
- setMessages(prev => [...prev, assistantResponse]);
111
+ // Simulate agent response (will be replaced by Mastra integration)
112
+ setTimeout(async () => {
113
+ await addMessage({ threadId: threadId as any, role: 'assistant', content: 'This response will be powered by your configured AI provider once Mastra integration is active. For now, messages are stored in Convex.' });
199
114
  setIsTyping(false);
200
115
  }, 1500);
201
116
  };
202
117
 
203
- const handleConfirmSendWithSecrets = () => {
204
- if (pendingMessage && secretWarning) {
205
- sendMessageToChat(pendingMessage, secretWarning);
118
+ const handleConfirmSendWithSecrets = async () => {
119
+ if (!pendingMessage || !secretWarning) return;
120
+ // Store secrets in vault
121
+ for (const secret of secretWarning) {
122
+ await storeSecret({ name: secret.name, category: secret.category, provider: secret.provider, value: secret.match });
206
123
  }
124
+ const censored = censorText(pendingMessage, secretWarning);
125
+ await sendMessage(censored);
207
126
  setSecretWarning(null);
208
127
  setPendingMessage(null);
209
128
  };
@@ -211,216 +130,143 @@ function ChatPageComponent() {
211
130
  const handleCancelSendWithSecrets = () => {
212
131
  setSecretWarning(null);
213
132
  setPendingMessage(null);
214
- inputRef.current?.focus();
215
133
  };
216
134
 
217
- const handleNewSession = () => {
218
- const newSessionId = `ses_${Date.now()}`;
219
- setCurrentSessionId(newSessionId);
220
- setMessages([]);
135
+ const handleNewChat = async () => {
136
+ setCurrentThreadId(null);
221
137
  };
222
138
 
223
- const currentAgent = agents.find(a => a.id === currentAgentId);
224
-
225
139
  return (
226
140
  <DashboardLayout>
227
- <div className="flex flex-col h-full bg-background">
228
- {/* Header */}
229
- <header className="flex items-center justify-between p-3 border-b border-border bg-card/50">
230
- <div className="flex items-center gap-3">
231
- <select
232
- value={currentSessionId || ''}
233
- onChange={(e) => setCurrentSessionId(e.target.value)}
234
- className="bg-background border border-border rounded-lg px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
235
- >
236
- {sessions.map((session) => (
237
- <option key={session.id} value={session.id}>{session.name}</option>
238
- ))}
239
- </select>
240
- <button onClick={handleNewSession} className="p-2 rounded-lg hover:bg-card border border-border" title="New Session">
241
- <Plus className="w-4 h-4 text-muted-foreground" />
141
+ <div className="flex h-[calc(100vh-8rem)] bg-background rounded-lg border border-border overflow-hidden">
142
+ {/* Sidebar */}
143
+ <aside className="w-64 border-r border-border bg-card flex flex-col">
144
+ <div className="p-3 border-b border-border">
145
+ <button onClick={handleNewChat} className="w-full bg-primary text-primary-foreground px-3 py-2 rounded-lg text-sm font-medium hover:bg-primary/90 flex items-center justify-center gap-2">
146
+ <Plus className="w-4 h-4" /> New Chat
242
147
  </button>
243
148
  </div>
244
- <div className="flex items-center gap-3">
245
- <div className="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded-lg">
246
- <div className={`w-2 h-2 rounded-full ${currentAgent?.provider === 'openai' ? 'bg-green-500' : currentAgent?.provider === 'anthropic' ? 'bg-orange-500' : currentAgent?.provider === 'xai' ? 'bg-purple-500' : 'bg-blue-500'}`} />
247
- <select
248
- value={currentAgentId || ''}
249
- onChange={(e) => setCurrentAgentId(e.target.value)}
250
- className="bg-transparent text-sm text-foreground focus:outline-none"
251
- >
252
- {agents.map((agent) => (
253
- <option key={agent.id} value={agent.id}>{agent.name}</option>
254
- ))}
255
- </select>
256
- </div>
257
- <button className="p-2 rounded-lg hover:bg-card border border-border" title="Chat Settings">
258
- <Settings2 className="w-4 h-4 text-muted-foreground" />
259
- </button>
149
+ <div className="p-3 border-b border-border">
150
+ <label className="text-xs font-medium text-muted-foreground mb-1 block">Agent</label>
151
+ <select value={currentAgentId || ''} onChange={(e) => setCurrentAgentId(e.target.value)} className="w-full bg-background border border-border rounded-md px-2 py-1.5 text-sm">
152
+ {agents.length === 0 ? (
153
+ <option value="">No agents — create one first</option>
154
+ ) : (
155
+ agents.map((a: any) => <option key={a.id} value={a.id}>{a.name}</option>)
156
+ )}
157
+ </select>
260
158
  </div>
261
- </header>
262
-
263
- {/* Vault Notification Banner */}
264
- {vaultNotification && (
265
- <div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-green-900/30 border border-green-700/50 rounded-lg text-green-400 text-sm animate-in fade-in slide-in-from-top-2">
266
- <Shield className="w-4 h-4 flex-shrink-0" />
267
- <span>{vaultNotification}</span>
268
- <button onClick={() => setVaultNotification(null)} className="ml-auto text-green-400/60 hover:text-green-400">&times;</button>
159
+ <div className="flex-1 overflow-y-auto p-2 space-y-1">
160
+ {threads.length === 0 ? (
161
+ <div className="text-center py-8 text-muted-foreground text-xs">No conversations yet</div>
162
+ ) : (
163
+ threads.map((t: any) => (
164
+ <button key={t._id} onClick={() => setCurrentThreadId(t._id)} className={`w-full text-left px-3 py-2 rounded-lg text-sm truncate transition-colors ${currentThreadId === t._id ? 'bg-primary/10 text-primary' : 'text-muted-foreground hover:bg-muted'}`}>
165
+ <MessageSquare className="w-3.5 h-3.5 inline mr-2" />
166
+ {t.name || `Thread ${t._id.slice(-6)}`}
167
+ </button>
168
+ ))
169
+ )}
269
170
  </div>
270
- )}
271
-
272
- {/* Messages Area */}
273
- <main className="flex-1 overflow-y-auto p-4 md:p-6">
274
- <div className="max-w-3xl mx-auto space-y-4">
275
- {messages.map((msg) => (
276
- <div key={msg.id} className={`flex items-end gap-2.5 ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`}>
277
- {msg.sender === 'assistant' && (
278
- <div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center flex-shrink-0">
279
- <Bot className="w-4 h-4 text-primary" />
280
- </div>
281
- )}
282
- <div className={`max-w-[75%] ${msg.sender === 'user' ? '' : ''}`}>
283
- <div className={`px-4 py-2.5 rounded-2xl ${
284
- msg.sender === 'user'
285
- ? 'bg-primary text-primary-foreground rounded-br-md'
286
- : msg.sender === 'system'
287
- ? 'bg-yellow-900/30 border border-yellow-700/50 text-yellow-200'
288
- : 'bg-card border border-border rounded-bl-md'
289
- }`}>
290
- {msg.isRedacted && (
291
- <div className="flex items-center gap-1.5 mb-1.5 text-xs opacity-75">
292
- <Lock className="w-3 h-3" />
293
- <span>Secrets redacted &amp; stored in Vault</span>
171
+ </aside>
172
+
173
+ {/* Main Chat Area */}
174
+ <div className="flex-1 flex flex-col">
175
+ <main className="flex-1 overflow-y-auto p-4">
176
+ {messages.length === 0 && !currentThreadId ? (
177
+ <div className="flex flex-col items-center justify-center h-full text-center">
178
+ <Bot className="w-16 h-16 text-muted-foreground/30 mb-4" />
179
+ <h2 className="text-xl font-semibold mb-2">Start a conversation</h2>
180
+ <p className="text-muted-foreground max-w-md">Select an agent and type a message to begin. Your conversations are stored in Convex and synced in real-time.</p>
181
+ </div>
182
+ ) : (
183
+ <div className="max-w-3xl mx-auto space-y-4">
184
+ {messages.map((msg: any) => (
185
+ <div key={msg._id} className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
186
+ {msg.role !== 'user' && (
187
+ <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
188
+ <Bot className="w-4 h-4 text-primary" />
189
+ </div>
190
+ )}
191
+ <div className={`max-w-[80%] px-4 py-3 rounded-2xl text-sm ${msg.role === 'user' ? 'bg-primary text-primary-foreground rounded-br-md' : 'bg-card border border-border rounded-bl-md'}`}>
192
+ {msg.content}
193
+ </div>
194
+ {msg.role === 'user' && (
195
+ <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
196
+ <User className="w-4 h-4 text-muted-foreground" />
294
197
  </div>
295
198
  )}
296
- <p className="text-sm whitespace-pre-wrap">{msg.text}</p>
297
199
  </div>
298
- {msg.secretsCaptured && msg.secretsCaptured.length > 0 && (
299
- <div className="mt-1.5 flex flex-wrap gap-1">
300
- {msg.secretsCaptured.map((s, i) => (
301
- <span key={i} className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-900/30 border border-green-700/40 rounded-full text-xs text-green-400">
302
- <Lock className="w-2.5 h-2.5" />
303
- {s.name}: {s.masked}
304
- </span>
305
- ))}
200
+ ))}
201
+ {isTyping && (
202
+ <div className="flex gap-3">
203
+ <div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center"><Bot className="w-4 h-4 text-primary" /></div>
204
+ <div className="bg-card border border-border rounded-2xl rounded-bl-md px-4 py-3">
205
+ <div className="flex space-x-1"><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" /><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.15s]" /><div className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce [animation-delay:0.3s]" /></div>
306
206
  </div>
307
- )}
308
- <p className={`text-xs mt-1 px-1 ${msg.sender === 'user' ? 'text-right text-muted-foreground' : 'text-muted-foreground'}`}>
309
- {msg.timestamp}
310
- </p>
311
- </div>
312
- {msg.sender === 'user' && (
313
- <div className="w-8 h-8 rounded-full bg-primary/20 border border-primary/30 flex items-center justify-center flex-shrink-0">
314
- <User className="w-4 h-4 text-primary" />
315
207
  </div>
316
208
  )}
317
- </div>
318
- ))}
319
-
320
- {isTyping && (
321
- <div className="flex items-end gap-2.5 justify-start">
322
- <div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center">
323
- <Bot className="w-4 h-4 text-primary" />
324
- </div>
325
- <div className="px-4 py-3 rounded-2xl rounded-bl-md bg-card border border-border">
326
- <div className="flex gap-1">
327
- <span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
328
- <span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
329
- <span className="w-2 h-2 bg-muted-foreground rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
330
- </div>
331
- </div>
209
+ <div ref={messagesEndRef} />
332
210
  </div>
333
211
  )}
334
- <div ref={messagesEndRef} />
335
- </div>
336
- </main>
337
-
338
- {/* Secret Warning Modal */}
339
- {secretWarning && (
340
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
341
- <div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md m-4 overflow-hidden">
342
- <div className="p-4 bg-yellow-900/30 border-b border-yellow-700/50 flex items-center gap-3">
343
- <ShieldAlert className="w-6 h-6 text-yellow-500" />
344
- <div>
345
- <h3 className="font-semibold text-foreground">Secrets Detected</h3>
346
- <p className="text-sm text-muted-foreground">Your message contains sensitive information</p>
212
+ </main>
213
+
214
+ {/* Secret Warning Modal */}
215
+ {secretWarning && (
216
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
217
+ <div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md m-4 overflow-hidden">
218
+ <div className="p-4 bg-yellow-900/30 border-b border-yellow-700/50 flex items-center gap-3">
219
+ <ShieldAlert className="w-6 h-6 text-yellow-500" />
220
+ <div>
221
+ <h3 className="font-semibold text-foreground">Secrets Detected</h3>
222
+ <p className="text-sm text-muted-foreground">Your message contains sensitive information</p>
223
+ </div>
347
224
  </div>
348
- </div>
349
- <div className="p-4 space-y-3">
350
- <p className="text-sm text-muted-foreground">
351
- The following secrets were detected in your message. They will be <strong className="text-foreground">automatically encrypted</strong> and stored in the Secure Vault. The original values will <strong className="text-foreground">never appear in chat history</strong>.
352
- </p>
353
- <div className="space-y-2">
354
- {secretWarning.map((secret, i) => (
355
- <div key={i} className="flex items-center gap-2 px-3 py-2 bg-background rounded-lg border border-border">
356
- <Lock className="w-4 h-4 text-yellow-500 flex-shrink-0" />
357
- <div className="min-w-0">
358
- <p className="text-sm font-medium text-foreground">{secret.name}</p>
359
- <p className="text-xs text-muted-foreground font-mono truncate">{maskSecret(secret.match)}</p>
225
+ <div className="p-4 space-y-3">
226
+ <p className="text-sm text-muted-foreground">
227
+ The following secrets were detected. They will be <strong className="text-foreground">automatically encrypted</strong> and stored in the Secure Vault. The original values will <strong className="text-foreground">never appear in chat history</strong>.
228
+ </p>
229
+ <div className="space-y-2">
230
+ {secretWarning.map((secret, i) => (
231
+ <div key={i} className="flex items-center gap-2 px-3 py-2 bg-background rounded-lg border border-border">
232
+ <Lock className="w-4 h-4 text-yellow-500 flex-shrink-0" />
233
+ <div className="min-w-0">
234
+ <p className="text-sm font-medium text-foreground">{secret.name}</p>
235
+ <p className="text-xs text-muted-foreground font-mono truncate">{maskSecret(secret.match)}</p>
236
+ </div>
237
+ <span className="ml-auto text-xs px-2 py-0.5 bg-yellow-900/30 text-yellow-400 rounded-full">{secret.category}</span>
360
238
  </div>
361
- <span className="ml-auto text-xs px-2 py-0.5 bg-yellow-900/30 text-yellow-400 rounded-full">{secret.category}</span>
362
- </div>
363
- ))}
239
+ ))}
240
+ </div>
241
+ </div>
242
+ <div className="p-4 border-t border-border flex justify-end gap-2">
243
+ <button onClick={handleCancelSendWithSecrets} className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80">Cancel</button>
244
+ <button onClick={handleConfirmSendWithSecrets} className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2">
245
+ <Shield className="w-4 h-4" /> Redact &amp; Send
246
+ </button>
364
247
  </div>
365
248
  </div>
366
- <div className="p-4 border-t border-border flex justify-end gap-2">
367
- <button
368
- onClick={handleCancelSendWithSecrets}
369
- className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80"
370
- >
371
- Cancel
372
- </button>
373
- <button
374
- onClick={handleConfirmSendWithSecrets}
375
- className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2"
376
- >
377
- <Shield className="w-4 h-4" />
378
- Redact &amp; Send
379
- </button>
380
- </div>
381
- </div>
382
- </div>
383
- )}
384
-
385
- {/* Input Area */}
386
- <footer className="p-3 border-t border-border bg-card/50">
387
- {/* Secret detection warning bar */}
388
- {inputHasSecrets.length > 0 && (
389
- <div className="mb-2 flex items-center gap-2 px-3 py-2 bg-yellow-900/20 border border-yellow-700/40 rounded-lg text-yellow-400 text-xs">
390
- <AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
391
- <span>
392
- <strong>{inputHasSecrets.length} secret{inputHasSecrets.length > 1 ? 's' : ''}</strong> detected ({inputHasSecrets.map(s => s.name).join(', ')}). Will be auto-redacted on send.
393
- </span>
394
249
  </div>
395
250
  )}
396
- <form onSubmit={handleSendMessage} className="max-w-3xl mx-auto flex items-center gap-2">
397
- <button type="button" className="p-2.5 rounded-lg hover:bg-background border border-border" title="Attach file">
398
- <Paperclip className="w-4 h-4 text-muted-foreground" />
399
- </button>
400
- <div className="flex-1 relative">
401
- <input
402
- ref={inputRef}
403
- type="text"
404
- value={input}
405
- onChange={(e) => setInput(e.target.value)}
406
- placeholder="Type your message..."
407
- className={`w-full bg-background border rounded-lg pl-4 pr-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary ${
408
- inputHasSecrets.length > 0 ? 'border-yellow-600/50' : 'border-border'
409
- }`}
410
- />
411
- </div>
412
- <button type="button" className="p-2.5 rounded-lg hover:bg-background border border-border" title="Voice input">
413
- <Mic className="w-4 h-4 text-muted-foreground" />
414
- </button>
415
- <button
416
- type="submit"
417
- disabled={!input.trim()}
418
- className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
419
- >
420
- <Send className="w-4 h-4" />
421
- </button>
422
- </form>
423
- </footer>
251
+
252
+ {/* Input Area */}
253
+ <footer className="p-3 border-t border-border bg-card/50">
254
+ {inputHasSecrets.length > 0 && (
255
+ <div className="mb-2 flex items-center gap-2 px-3 py-2 bg-yellow-900/20 border border-yellow-700/40 rounded-lg text-yellow-400 text-xs">
256
+ <AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
257
+ <span><strong>{inputHasSecrets.length} secret{inputHasSecrets.length > 1 ? 's' : ''}</strong> detected. Will be auto-redacted on send.</span>
258
+ </div>
259
+ )}
260
+ <form onSubmit={handleSendMessage} className="max-w-3xl mx-auto flex items-center gap-2">
261
+ <div className="flex-1 relative">
262
+ <input ref={inputRef} type="text" value={input} onChange={(e) => setInput(e.target.value)} placeholder={agents.length === 0 ? "Create an agent first..." : "Type your message..."} disabled={agents.length === 0} className={`w-full bg-background border rounded-lg pl-4 pr-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary disabled:opacity-50 ${inputHasSecrets.length > 0 ? 'border-yellow-600/50' : 'border-border'}`} />
263
+ </div>
264
+ <button type="submit" disabled={!input.trim() || agents.length === 0} className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed">
265
+ <Send className="w-4 h-4" />
266
+ </button>
267
+ </form>
268
+ </footer>
269
+ </div>
424
270
  </div>
425
271
  </DashboardLayout>
426
272
  );