@agentforge-ai/cli 0.4.0 → 0.4.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 (79) hide show
  1. package/dist/default/.env.example +11 -0
  2. package/dist/default/dashboard/app/components/DashboardLayout.tsx +245 -0
  3. package/dist/default/dashboard/app/components/ui/badge.tsx +26 -0
  4. package/dist/default/dashboard/app/components/ui/button.tsx +41 -0
  5. package/dist/default/dashboard/app/components/ui/card.tsx +44 -0
  6. package/dist/default/dashboard/app/components/ui/dialog.tsx +66 -0
  7. package/dist/default/dashboard/app/components/ui/input.tsx +21 -0
  8. package/dist/default/dashboard/app/components/ui/label.tsx +18 -0
  9. package/dist/default/dashboard/app/components/ui/select.tsx +75 -0
  10. package/dist/default/dashboard/app/components/ui/sheet.tsx +73 -0
  11. package/dist/default/dashboard/app/components/ui/switch.tsx +34 -0
  12. package/dist/default/dashboard/app/components/ui/table.tsx +60 -0
  13. package/dist/default/dashboard/app/components/ui/tabs.tsx +50 -0
  14. package/dist/default/dashboard/app/components/ui/tooltip.tsx +23 -0
  15. package/dist/default/dashboard/app/lib/utils.ts +6 -0
  16. package/dist/default/dashboard/app/main.tsx +35 -0
  17. package/dist/default/dashboard/app/routeTree.gen.ts +352 -0
  18. package/dist/default/dashboard/app/routes/__root.tsx +10 -0
  19. package/dist/default/dashboard/app/routes/agents.tsx +255 -0
  20. package/dist/default/dashboard/app/routes/chat.tsx +427 -0
  21. package/dist/default/dashboard/app/routes/connections.tsx +413 -0
  22. package/dist/default/dashboard/app/routes/cron.tsx +322 -0
  23. package/dist/default/dashboard/app/routes/files.tsx +203 -0
  24. package/dist/default/dashboard/app/routes/index.tsx +141 -0
  25. package/dist/default/dashboard/app/routes/projects.tsx +254 -0
  26. package/dist/default/dashboard/app/routes/sessions.tsx +272 -0
  27. package/dist/default/dashboard/app/routes/settings.tsx +583 -0
  28. package/dist/default/dashboard/app/routes/skills.tsx +252 -0
  29. package/dist/default/dashboard/app/routes/usage.tsx +181 -0
  30. package/dist/default/dashboard/app/styles/globals.css +93 -0
  31. package/dist/default/dashboard/index.html +13 -0
  32. package/dist/default/dashboard/package.json +36 -0
  33. package/dist/default/dashboard/postcss.config.js +6 -0
  34. package/dist/default/dashboard/tailwind.config.js +50 -0
  35. package/dist/default/dashboard/tsconfig.json +24 -0
  36. package/dist/default/dashboard/vite.config.ts +16 -0
  37. package/dist/default/package.json +5 -2
  38. package/dist/default/src/agent.ts +42 -2
  39. package/dist/index.js +135 -22
  40. package/dist/index.js.map +1 -1
  41. package/package.json +1 -1
  42. package/templates/default/.env.example +11 -0
  43. package/templates/default/dashboard/app/components/DashboardLayout.tsx +245 -0
  44. package/templates/default/dashboard/app/components/ui/badge.tsx +26 -0
  45. package/templates/default/dashboard/app/components/ui/button.tsx +41 -0
  46. package/templates/default/dashboard/app/components/ui/card.tsx +44 -0
  47. package/templates/default/dashboard/app/components/ui/dialog.tsx +66 -0
  48. package/templates/default/dashboard/app/components/ui/input.tsx +21 -0
  49. package/templates/default/dashboard/app/components/ui/label.tsx +18 -0
  50. package/templates/default/dashboard/app/components/ui/select.tsx +75 -0
  51. package/templates/default/dashboard/app/components/ui/sheet.tsx +73 -0
  52. package/templates/default/dashboard/app/components/ui/switch.tsx +34 -0
  53. package/templates/default/dashboard/app/components/ui/table.tsx +60 -0
  54. package/templates/default/dashboard/app/components/ui/tabs.tsx +50 -0
  55. package/templates/default/dashboard/app/components/ui/tooltip.tsx +23 -0
  56. package/templates/default/dashboard/app/lib/utils.ts +6 -0
  57. package/templates/default/dashboard/app/main.tsx +35 -0
  58. package/templates/default/dashboard/app/routeTree.gen.ts +352 -0
  59. package/templates/default/dashboard/app/routes/__root.tsx +10 -0
  60. package/templates/default/dashboard/app/routes/agents.tsx +255 -0
  61. package/templates/default/dashboard/app/routes/chat.tsx +427 -0
  62. package/templates/default/dashboard/app/routes/connections.tsx +413 -0
  63. package/templates/default/dashboard/app/routes/cron.tsx +322 -0
  64. package/templates/default/dashboard/app/routes/files.tsx +203 -0
  65. package/templates/default/dashboard/app/routes/index.tsx +141 -0
  66. package/templates/default/dashboard/app/routes/projects.tsx +254 -0
  67. package/templates/default/dashboard/app/routes/sessions.tsx +272 -0
  68. package/templates/default/dashboard/app/routes/settings.tsx +583 -0
  69. package/templates/default/dashboard/app/routes/skills.tsx +252 -0
  70. package/templates/default/dashboard/app/routes/usage.tsx +181 -0
  71. package/templates/default/dashboard/app/styles/globals.css +93 -0
  72. package/templates/default/dashboard/index.html +13 -0
  73. package/templates/default/dashboard/package.json +36 -0
  74. package/templates/default/dashboard/postcss.config.js +6 -0
  75. package/templates/default/dashboard/tailwind.config.js +50 -0
  76. package/templates/default/dashboard/tsconfig.json +24 -0
  77. package/templates/default/dashboard/vite.config.ts +16 -0
  78. package/templates/default/package.json +5 -2
  79. package/templates/default/src/agent.ts +42 -2
@@ -0,0 +1,427 @@
1
+ import { createFileRoute } from '@tanstack/react-router';
2
+ import { DashboardLayout } from '../components/DashboardLayout';
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { Send, Plus, Bot, User, Shield, ShieldAlert, Lock, AlertTriangle, Paperclip, Mic, Settings2 } from 'lucide-react';
5
+
6
+ // ============================================================
7
+ // SECRET DETECTION ENGINE (client-side mirror of vault patterns)
8
+ // ============================================================
9
+ const SECRET_PATTERNS = [
10
+ { pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openai", name: "OpenAI API Key" },
11
+ { pattern: /sk-ant-[a-zA-Z0-9-]{20,}/g, category: "api_key", provider: "anthropic", name: "Anthropic API Key" },
12
+ { pattern: /sk-or-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openrouter", name: "OpenRouter API Key" },
13
+ { pattern: /AIza[a-zA-Z0-9_-]{35}/g, category: "api_key", provider: "google", name: "Google API Key" },
14
+ { pattern: /xai-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "xai", name: "xAI API Key" },
15
+ { 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
+ { pattern: /AKIA[A-Z0-9]{16}/g, category: "credential", provider: "aws", name: "AWS Access Key" },
21
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Live Key" },
22
+ { 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
+ ];
26
+
27
+ interface DetectedSecret {
28
+ match: string;
29
+ category: string;
30
+ provider: string;
31
+ name: string;
32
+ }
33
+
34
+ function detectSecrets(text: string): DetectedSecret[] {
35
+ const detected: DetectedSecret[] = [];
36
+ for (const { pattern, category, provider, name } of SECRET_PATTERNS) {
37
+ const regex = new RegExp(pattern.source, pattern.flags);
38
+ let match;
39
+ while ((match = regex.exec(text)) !== null) {
40
+ detected.push({ match: match[0], category, provider, name });
41
+ }
42
+ }
43
+ return detected;
44
+ }
45
+
46
+ function maskSecret(value: string): string {
47
+ if (value.length <= 12) return value.substring(0, 3) + "..." + value.substring(value.length - 3);
48
+ return value.substring(0, 6) + "..." + value.substring(value.length - 4);
49
+ }
50
+
51
+ function censorText(text: string, secrets: DetectedSecret[]): string {
52
+ let censored = text;
53
+ for (const secret of secrets) {
54
+ censored = censored.replace(secret.match, `[🔒 ${secret.name}: ${maskSecret(secret.match)}]`);
55
+ }
56
+ return censored;
57
+ }
58
+
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
+ export const Route = createFileRoute('/chat')({ component: ChatPageComponent });
88
+
89
+ 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[]>([]);
94
+ const [input, setInput] = useState('');
95
+ const [isTyping, setIsTyping] = useState(false);
96
+ const [currentAgentId, setCurrentAgentId] = useState(mockAgents[0]?.id || null);
97
+ const [secretWarning, setSecretWarning] = useState<DetectedSecret[] | null>(null);
98
+ const [pendingMessage, setPendingMessage] = useState<string | null>(null);
99
+ const [vaultNotification, setVaultNotification] = useState<string | null>(null);
100
+
101
+ const messagesEndRef = useRef<HTMLDivElement>(null);
102
+ const inputRef = useRef<HTMLInputElement>(null);
103
+
104
+ const scrollToBottom = () => {
105
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
106
+ };
107
+
108
+ useEffect(() => { scrollToBottom(); }, [messages, isTyping]);
109
+
110
+ 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);
124
+ }
125
+ }, [currentSessionId]);
126
+
127
+ // Real-time secret detection as user types
128
+ const inputHasSecrets = input.length > 8 ? detectSecrets(input) : [];
129
+
130
+ const handleSendMessage = useCallback((e: React.FormEvent) => {
131
+ e.preventDefault();
132
+ if (!input.trim() || !currentSessionId) return;
133
+
134
+ const detectedSecrets = detectSecrets(input);
135
+
136
+ if (detectedSecrets.length > 0) {
137
+ // Show warning before sending
138
+ setSecretWarning(detectedSecrets);
139
+ setPendingMessage(input);
140
+ return;
141
+ }
142
+
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 });
160
+
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);
166
+ }
167
+
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]);
178
+ setInput('');
179
+ setIsTyping(true);
180
+
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]);
199
+ setIsTyping(false);
200
+ }, 1500);
201
+ };
202
+
203
+ const handleConfirmSendWithSecrets = () => {
204
+ if (pendingMessage && secretWarning) {
205
+ sendMessageToChat(pendingMessage, secretWarning);
206
+ }
207
+ setSecretWarning(null);
208
+ setPendingMessage(null);
209
+ };
210
+
211
+ const handleCancelSendWithSecrets = () => {
212
+ setSecretWarning(null);
213
+ setPendingMessage(null);
214
+ inputRef.current?.focus();
215
+ };
216
+
217
+ const handleNewSession = () => {
218
+ const newSessionId = `ses_${Date.now()}`;
219
+ setCurrentSessionId(newSessionId);
220
+ setMessages([]);
221
+ };
222
+
223
+ const currentAgent = agents.find(a => a.id === currentAgentId);
224
+
225
+ return (
226
+ <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" />
242
+ </button>
243
+ </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>
260
+ </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>
269
+ </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>
294
+ </div>
295
+ )}
296
+ <p className="text-sm whitespace-pre-wrap">{msg.text}</p>
297
+ </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
+ ))}
306
+ </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
+ </div>
316
+ )}
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>
332
+ </div>
333
+ )}
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>
347
+ </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>
360
+ </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
+ ))}
364
+ </div>
365
+ </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
+ </div>
395
+ )}
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>
424
+ </div>
425
+ </DashboardLayout>
426
+ );
427
+ }