@agentforge-ai/cli 0.5.4 → 0.6.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.
@@ -1,12 +1,27 @@
1
- import { createFileRoute } from '@tanstack/react-router';
2
- import { DashboardLayout } from '../components/DashboardLayout';
3
- import { useState, useEffect, useRef, useCallback } from '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';
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { DashboardLayout } from "../components/DashboardLayout";
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+ import { useQuery, useMutation, useAction } from "convex/react";
5
+ import { api } from "../../convex/_generated/api";
6
+ import {
7
+ Send,
8
+ Plus,
9
+ Bot,
10
+ User,
11
+ Shield,
12
+ ShieldAlert,
13
+ Lock,
14
+ AlertTriangle,
15
+ Paperclip,
16
+ Mic,
17
+ Settings2,
18
+ Loader2,
19
+ MessageSquare,
20
+ Trash2,
21
+ } from "lucide-react";
7
22
 
8
23
  // ============================================================
9
- // SECRET DETECTION ENGINE
24
+ // SECRET DETECTION ENGINE (client-side mirror of vault patterns)
10
25
  // ============================================================
11
26
  const SECRET_PATTERNS = [
12
27
  { pattern: /sk-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "openai", name: "OpenAI API Key" },
@@ -15,12 +30,22 @@ const SECRET_PATTERNS = [
15
30
  { pattern: /AIza[a-zA-Z0-9_-]{35}/g, category: "api_key", provider: "google", name: "Google API Key" },
16
31
  { pattern: /xai-[a-zA-Z0-9]{20,}/g, category: "api_key", provider: "xai", name: "xAI API Key" },
17
32
  { pattern: /ghp_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub Token" },
33
+ { pattern: /gho_[a-zA-Z0-9]{36}/g, category: "token", provider: "github", name: "GitHub OAuth" },
34
+ { pattern: /glpat-[a-zA-Z0-9_-]{20,}/g, category: "token", provider: "gitlab", name: "GitLab Token" },
35
+ { pattern: /xoxb-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack Bot Token" },
36
+ { pattern: /xoxp-[a-zA-Z0-9-]+/g, category: "token", provider: "slack", name: "Slack User Token" },
18
37
  { pattern: /AKIA[A-Z0-9]{16}/g, category: "credential", provider: "aws", name: "AWS Access Key" },
19
38
  { pattern: /sk_live_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Live Key" },
20
39
  { pattern: /sk_test_[a-zA-Z0-9]{24,}/g, category: "api_key", provider: "stripe", name: "Stripe Test Key" },
40
+ { pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, category: "api_key", provider: "sendgrid", name: "SendGrid Key" },
21
41
  ];
22
42
 
23
- interface DetectedSecret { match: string; category: string; provider: string; name: string; }
43
+ interface DetectedSecret {
44
+ match: string;
45
+ category: string;
46
+ provider: string;
47
+ name: string;
48
+ }
24
49
 
25
50
  function detectSecrets(text: string): DetectedSecret[] {
26
51
  const detected: DetectedSecret[] = [];
@@ -42,87 +67,157 @@ function maskSecret(value: string): string {
42
67
  function censorText(text: string, secrets: DetectedSecret[]): string {
43
68
  let censored = text;
44
69
  for (const secret of secrets) {
45
- censored = censored.replace(secret.match, `[REDACTED: ${secret.name}]`);
70
+ censored = censored.replace(secret.match, `[${secret.name}: ${maskSecret(secret.match)}]`);
46
71
  }
47
72
  return censored;
48
73
  }
49
74
 
50
- export const Route = createFileRoute('/chat')({ component: ChatPageComponent });
75
+ // ============================================================
76
+ // Chat Page Component
77
+ // ============================================================
78
+ export const Route = createFileRoute("/chat")({ component: ChatPageComponent });
51
79
 
52
80
  function ChatPageComponent() {
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);
81
+ // ── Convex queries ──────────────────────────────────────────
82
+ const agents = useQuery(api.agents.listActive, {}) ?? [];
83
+ const threads = useQuery(api.chat.listThreads, {}) ?? [];
58
84
 
85
+ // ── Convex mutations & actions ──────────────────────────────
86
+ const createThread = useMutation(api.chat.createThread);
87
+ const sendMessageAction = useAction(api.chat.sendMessage);
88
+
89
+ // ── Local state ─────────────────────────────────────────────
59
90
  const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
60
91
  const [currentAgentId, setCurrentAgentId] = useState<string | null>(null);
61
- const [input, setInput] = useState('');
62
- const [isTyping, setIsTyping] = useState(false);
92
+ const [input, setInput] = useState("");
93
+ const [isGenerating, setIsGenerating] = useState(false);
63
94
  const [secretWarning, setSecretWarning] = useState<DetectedSecret[] | null>(null);
64
95
  const [pendingMessage, setPendingMessage] = useState<string | null>(null);
96
+ const [vaultNotification, setVaultNotification] = useState<string | null>(null);
97
+ const [error, setError] = useState<string | null>(null);
98
+
65
99
  const messagesEndRef = useRef<HTMLDivElement>(null);
66
100
  const inputRef = useRef<HTMLInputElement>(null);
67
101
 
68
- // Load messages for current thread
69
- const messages = useQuery(api.messages.list, currentThreadId ? { threadId: currentThreadId as any } : "skip") ?? [];
102
+ // ── Subscribe to messages for the current thread ────────────
103
+ // This is the real-time subscription messages update automatically
104
+ // when new ones are inserted by the Convex action.
105
+ const messages = useQuery(
106
+ api.chat.getThreadMessages,
107
+ currentThreadId ? { threadId: currentThreadId as any } : "skip"
108
+ ) ?? [];
70
109
 
71
- // Auto-select first agent
110
+ // ── Auto-select first agent ─────────────────────────────────
72
111
  useEffect(() => {
73
112
  if (agents.length > 0 && !currentAgentId) {
74
113
  setCurrentAgentId(agents[0].id);
75
114
  }
76
115
  }, [agents, currentAgentId]);
77
116
 
78
- // Auto-scroll
117
+ // ── Auto-select first thread or none ────────────────────────
79
118
  useEffect(() => {
80
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
81
- }, [messages]);
119
+ if (threads.length > 0 && !currentThreadId) {
120
+ setCurrentThreadId(threads[0]._id);
121
+ }
122
+ }, [threads, currentThreadId]);
82
123
 
83
- const inputHasSecrets = input ? detectSecrets(input) : [];
124
+ // ── Auto-scroll to bottom ───────────────────────────────────
125
+ const scrollToBottom = () => {
126
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
127
+ };
128
+ useEffect(() => {
129
+ scrollToBottom();
130
+ }, [messages, isGenerating]);
84
131
 
85
- const handleSendMessage = async (e: React.FormEvent) => {
86
- e.preventDefault();
87
- if (!input.trim()) return;
132
+ // ── Secret detection as user types ──────────────────────────
133
+ const inputHasSecrets = input.length > 8 ? detectSecrets(input) : [];
88
134
 
89
- const secrets = detectSecrets(input);
90
- if (secrets.length > 0) {
91
- setSecretWarning(secrets);
92
- setPendingMessage(input);
135
+ // ── Create new thread ───────────────────────────────────────
136
+ const handleNewThread = useCallback(async () => {
137
+ if (!currentAgentId) {
138
+ setError("Please select an agent first.");
93
139
  return;
94
140
  }
141
+ try {
142
+ const agent = agents.find((a) => a.id === currentAgentId);
143
+ const threadId = await createThread({
144
+ agentId: currentAgentId,
145
+ name: `Chat with ${agent?.name || "Agent"}`,
146
+ });
147
+ setCurrentThreadId(threadId);
148
+ setError(null);
149
+ } catch (e) {
150
+ setError(`Failed to create thread: ${e instanceof Error ? e.message : String(e)}`);
151
+ }
152
+ }, [currentAgentId, agents, createThread]);
95
153
 
96
- await sendMessage(input);
97
- };
154
+ // ── Send message ────────────────────────────────────────────
155
+ const handleSendMessage = useCallback(
156
+ async (e: React.FormEvent) => {
157
+ e.preventDefault();
158
+ if (!input.trim() || !currentAgentId) return;
98
159
 
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);
160
+ // Check for secrets
161
+ const detectedSecrets = detectSecrets(input);
162
+ if (detectedSecrets.length > 0) {
163
+ setSecretWarning(detectedSecrets);
164
+ setPendingMessage(input);
165
+ return;
166
+ }
167
+
168
+ await sendMessageToChat(input);
169
+ },
170
+ [input, currentAgentId, currentThreadId]
171
+ );
172
+
173
+ const sendMessageToChat = async (text: string, secrets?: DetectedSecret[]) => {
174
+ if (!currentAgentId) return;
175
+
176
+ let messageText = text;
177
+ if (secrets && secrets.length > 0) {
178
+ messageText = censorText(text, secrets);
179
+ setVaultNotification(
180
+ `${secrets.length} secret${secrets.length > 1 ? "s" : ""} detected and redacted.`
181
+ );
182
+ setTimeout(() => setVaultNotification(null), 5000);
104
183
  }
105
- if (!threadId) return;
106
184
 
107
- await addMessage({ threadId: threadId as any, role: 'user', content: text });
108
- setInput('');
109
- setIsTyping(true);
185
+ setInput("");
186
+ setIsGenerating(true);
187
+ setError(null);
188
+
189
+ try {
190
+ // If no thread exists, create one first
191
+ let threadId = currentThreadId;
192
+ if (!threadId) {
193
+ const agent = agents.find((a) => a.id === currentAgentId);
194
+ threadId = await createThread({
195
+ agentId: currentAgentId,
196
+ name: `Chat with ${agent?.name || "Agent"}`,
197
+ });
198
+ setCurrentThreadId(threadId);
199
+ }
110
200
 
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.' });
114
- setIsTyping(false);
115
- }, 1500);
201
+ // Call the Convex action this stores user message, calls LLM,
202
+ // and stores assistant response. The useQuery subscription above
203
+ // will automatically pick up both new messages in real-time.
204
+ await sendMessageAction({
205
+ agentId: currentAgentId,
206
+ threadId: threadId as any,
207
+ content: messageText,
208
+ });
209
+ } catch (e) {
210
+ const msg = e instanceof Error ? e.message : String(e);
211
+ setError(`Failed to send message: ${msg}`);
212
+ } finally {
213
+ setIsGenerating(false);
214
+ }
116
215
  };
117
216
 
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 });
217
+ const handleConfirmSendWithSecrets = () => {
218
+ if (pendingMessage && secretWarning) {
219
+ sendMessageToChat(pendingMessage, secretWarning);
123
220
  }
124
- const censored = censorText(pendingMessage, secretWarning);
125
- await sendMessage(censored);
126
221
  setSecretWarning(null);
127
222
  setPendingMessage(null);
128
223
  };
@@ -130,143 +225,343 @@ function ChatPageComponent() {
130
225
  const handleCancelSendWithSecrets = () => {
131
226
  setSecretWarning(null);
132
227
  setPendingMessage(null);
228
+ inputRef.current?.focus();
133
229
  };
134
230
 
135
- const handleNewChat = async () => {
136
- setCurrentThreadId(null);
137
- };
231
+ // ── Derived state ───────────────────────────────────────────
232
+ const currentAgent = agents.find((a) => a.id === currentAgentId);
233
+ const hasAgents = agents.length > 0;
138
234
 
139
235
  return (
140
236
  <DashboardLayout>
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
147
- </button>
148
- </div>
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>)
237
+ <div className="flex flex-col h-full bg-background">
238
+ {/* Header */}
239
+ <header className="flex items-center justify-between p-3 border-b border-border bg-card/50">
240
+ <div className="flex items-center gap-3">
241
+ {/* Thread selector */}
242
+ <select
243
+ value={currentThreadId || ""}
244
+ onChange={(e) => setCurrentThreadId(e.target.value || null)}
245
+ 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"
246
+ >
247
+ {threads.length === 0 && (
248
+ <option value="">No threads yet</option>
156
249
  )}
250
+ {threads.map((thread) => (
251
+ <option key={thread._id} value={thread._id}>
252
+ {thread.name || "Untitled Thread"}
253
+ </option>
254
+ ))}
157
255
  </select>
256
+ <button
257
+ onClick={handleNewThread}
258
+ className="p-2 rounded-lg hover:bg-card border border-border"
259
+ title="New Thread"
260
+ >
261
+ <Plus className="w-4 h-4 text-muted-foreground" />
262
+ </button>
158
263
  </div>
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
- )}
264
+ <div className="flex items-center gap-3">
265
+ {/* Agent selector */}
266
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-background border border-border rounded-lg">
267
+ <div
268
+ className={`w-2 h-2 rounded-full ${
269
+ currentAgent?.provider === "openai"
270
+ ? "bg-green-500"
271
+ : currentAgent?.provider === "anthropic"
272
+ ? "bg-orange-500"
273
+ : currentAgent?.provider === "openrouter"
274
+ ? "bg-purple-500"
275
+ : "bg-blue-500"
276
+ }`}
277
+ />
278
+ <select
279
+ value={currentAgentId || ""}
280
+ onChange={(e) => setCurrentAgentId(e.target.value || null)}
281
+ className="bg-transparent text-sm text-foreground focus:outline-none"
282
+ >
283
+ {agents.length === 0 && (
284
+ <option value="">No agents configured</option>
285
+ )}
286
+ {agents.map((agent) => (
287
+ <option key={agent.id} value={agent.id}>
288
+ {agent.name} ({agent.provider}/{agent.model})
289
+ </option>
290
+ ))}
291
+ </select>
292
+ </div>
293
+ <button
294
+ className="p-2 rounded-lg hover:bg-card border border-border"
295
+ title="Chat Settings"
296
+ >
297
+ <Settings2 className="w-4 h-4 text-muted-foreground" />
298
+ </button>
170
299
  </div>
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>
300
+ </header>
301
+
302
+ {/* Vault Notification Banner */}
303
+ {vaultNotification && (
304
+ <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">
305
+ <Shield className="w-4 h-4 flex-shrink-0" />
306
+ <span>{vaultNotification}</span>
307
+ <button
308
+ onClick={() => setVaultNotification(null)}
309
+ className="ml-auto text-green-400/60 hover:text-green-400"
310
+ >
311
+ &times;
312
+ </button>
313
+ </div>
314
+ )}
315
+
316
+ {/* Error Banner */}
317
+ {error && (
318
+ <div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-red-900/30 border border-red-700/50 rounded-lg text-red-400 text-sm">
319
+ <AlertTriangle className="w-4 h-4 flex-shrink-0" />
320
+ <span>{error}</span>
321
+ <button
322
+ onClick={() => setError(null)}
323
+ className="ml-auto text-red-400/60 hover:text-red-400"
324
+ >
325
+ &times;
326
+ </button>
327
+ </div>
328
+ )}
329
+
330
+ {/* No Agents Warning */}
331
+ {!hasAgents && (
332
+ <div className="mx-4 mt-3 flex items-center gap-2 px-4 py-2.5 bg-yellow-900/30 border border-yellow-700/50 rounded-lg text-yellow-400 text-sm">
333
+ <AlertTriangle className="w-4 h-4 flex-shrink-0" />
334
+ <span>
335
+ No agents configured. Go to the{" "}
336
+ <a href="/agents" className="underline font-medium">
337
+ Agents
338
+ </a>{" "}
339
+ page to create one, or the chat will use a default assistant.
340
+ </span>
341
+ </div>
342
+ )}
343
+
344
+ {/* Messages Area */}
345
+ <main className="flex-1 overflow-y-auto p-4 md:p-6">
346
+ <div className="max-w-3xl mx-auto space-y-4">
347
+ {/* Empty state */}
348
+ {messages.length === 0 && !isGenerating && (
349
+ <div className="flex flex-col items-center justify-center py-20 text-center">
350
+ <div className="w-16 h-16 rounded-full bg-card border border-border flex items-center justify-center mb-4">
351
+ <MessageSquare className="w-8 h-8 text-primary" />
352
+ </div>
353
+ <h3 className="text-lg font-semibold text-foreground mb-2">
354
+ {currentThreadId ? "No messages yet" : "Start a new conversation"}
355
+ </h3>
356
+ <p className="text-sm text-muted-foreground max-w-md">
357
+ {currentThreadId
358
+ ? "Send a message to start chatting with your agent."
359
+ : "Select a thread from the dropdown or create a new one to begin."}
360
+ </p>
181
361
  </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" />
197
- </div>
198
- )}
362
+ )}
363
+
364
+ {/* Message list */}
365
+ {messages.map((msg) => (
366
+ <div
367
+ key={msg._id}
368
+ className={`flex items-end gap-2.5 ${
369
+ msg.role === "user" ? "justify-end" : "justify-start"
370
+ }`}
371
+ >
372
+ {msg.role === "assistant" && (
373
+ <div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center flex-shrink-0">
374
+ <Bot className="w-4 h-4 text-primary" />
199
375
  </div>
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>
206
- </div>
376
+ )}
377
+ <div className={`max-w-[75%]`}>
378
+ <div
379
+ className={`px-4 py-2.5 rounded-2xl ${
380
+ msg.role === "user"
381
+ ? "bg-primary text-primary-foreground rounded-br-md"
382
+ : msg.role === "system"
383
+ ? "bg-yellow-900/30 border border-yellow-700/50 text-yellow-200"
384
+ : "bg-card border border-border rounded-bl-md"
385
+ }`}
386
+ >
387
+ <p className="text-sm whitespace-pre-wrap">{msg.content}</p>
388
+ </div>
389
+ <p
390
+ className={`text-xs mt-1 px-1 ${
391
+ msg.role === "user"
392
+ ? "text-right text-muted-foreground"
393
+ : "text-muted-foreground"
394
+ }`}
395
+ >
396
+ {new Date(msg.createdAt).toLocaleTimeString([], {
397
+ hour: "2-digit",
398
+ minute: "2-digit",
399
+ })}
400
+ </p>
401
+ </div>
402
+ {msg.role === "user" && (
403
+ <div className="w-8 h-8 rounded-full bg-primary/20 border border-primary/30 flex items-center justify-center flex-shrink-0">
404
+ <User className="w-4 h-4 text-primary" />
207
405
  </div>
208
406
  )}
209
- <div ref={messagesEndRef} />
210
407
  </div>
211
- )}
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>
408
+ ))}
409
+
410
+ {/* Typing indicator */}
411
+ {isGenerating && (
412
+ <div className="flex items-end gap-2.5 justify-start">
413
+ <div className="w-8 h-8 rounded-full bg-card border border-border flex items-center justify-center">
414
+ <Bot className="w-4 h-4 text-primary" />
415
+ </div>
416
+ <div className="px-4 py-3 rounded-2xl rounded-bl-md bg-card border border-border">
417
+ <div className="flex items-center gap-2">
418
+ <Loader2 className="w-4 h-4 animate-spin text-primary" />
419
+ <span className="text-sm text-muted-foreground">
420
+ {currentAgent?.name || "Agent"} is thinking...
421
+ </span>
223
422
  </div>
224
423
  </div>
225
- <div className="p-4 space-y-3">
424
+ </div>
425
+ )}
426
+ <div ref={messagesEndRef} />
427
+ </div>
428
+ </main>
429
+
430
+ {/* Secret Warning Modal */}
431
+ {secretWarning && (
432
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
433
+ <div className="bg-card border border-border rounded-xl shadow-2xl w-full max-w-md m-4 overflow-hidden">
434
+ <div className="p-4 bg-yellow-900/30 border-b border-yellow-700/50 flex items-center gap-3">
435
+ <ShieldAlert className="w-6 h-6 text-yellow-500" />
436
+ <div>
437
+ <h3 className="font-semibold text-foreground">
438
+ Secrets Detected
439
+ </h3>
226
440
  <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>.
441
+ Your message contains sensitive information
228
442
  </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>
238
- </div>
239
- ))}
240
- </div>
241
443
  </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>
444
+ </div>
445
+ <div className="p-4 space-y-3">
446
+ <p className="text-sm text-muted-foreground">
447
+ The following secrets were detected in your message. They will
448
+ be{" "}
449
+ <strong className="text-foreground">
450
+ automatically redacted
451
+ </strong>{" "}
452
+ before being sent. The original values will{" "}
453
+ <strong className="text-foreground">
454
+ never appear in chat history
455
+ </strong>
456
+ .
457
+ </p>
458
+ <div className="space-y-2">
459
+ {secretWarning.map((secret, i) => (
460
+ <div
461
+ key={i}
462
+ className="flex items-center gap-2 px-3 py-2 bg-background rounded-lg border border-border"
463
+ >
464
+ <Lock className="w-4 h-4 text-yellow-500 flex-shrink-0" />
465
+ <div className="min-w-0">
466
+ <p className="text-sm font-medium text-foreground">
467
+ {secret.name}
468
+ </p>
469
+ <p className="text-xs text-muted-foreground font-mono truncate">
470
+ {maskSecret(secret.match)}
471
+ </p>
472
+ </div>
473
+ <span className="ml-auto text-xs px-2 py-0.5 bg-yellow-900/30 text-yellow-400 rounded-full">
474
+ {secret.category}
475
+ </span>
476
+ </div>
477
+ ))}
247
478
  </div>
248
479
  </div>
480
+ <div className="p-4 border-t border-border flex justify-end gap-2">
481
+ <button
482
+ onClick={handleCancelSendWithSecrets}
483
+ className="px-4 py-2 text-sm rounded-lg bg-muted text-muted-foreground hover:bg-muted/80"
484
+ >
485
+ Cancel
486
+ </button>
487
+ <button
488
+ onClick={handleConfirmSendWithSecrets}
489
+ className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2"
490
+ >
491
+ <Shield className="w-4 h-4" />
492
+ Redact &amp; Send
493
+ </button>
494
+ </div>
249
495
  </div>
250
- )}
496
+ </div>
497
+ )}
251
498
 
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">
499
+ {/* Input Area */}
500
+ <footer className="p-3 border-t border-border bg-card/50">
501
+ {/* Secret detection warning bar */}
502
+ {inputHasSecrets.length > 0 && (
503
+ <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">
504
+ <AlertTriangle className="w-3.5 h-3.5 flex-shrink-0" />
505
+ <span>
506
+ <strong>
507
+ {inputHasSecrets.length} secret
508
+ {inputHasSecrets.length > 1 ? "s" : ""}
509
+ </strong>{" "}
510
+ detected ({inputHasSecrets.map((s) => s.name).join(", ")}). Will
511
+ be auto-redacted on send.
512
+ </span>
513
+ </div>
514
+ )}
515
+ <form
516
+ onSubmit={handleSendMessage}
517
+ className="max-w-3xl mx-auto flex items-center gap-2"
518
+ >
519
+ <button
520
+ type="button"
521
+ className="p-2.5 rounded-lg hover:bg-background border border-border"
522
+ title="Attach file"
523
+ >
524
+ <Paperclip className="w-4 h-4 text-muted-foreground" />
525
+ </button>
526
+ <div className="flex-1 relative">
527
+ <input
528
+ ref={inputRef}
529
+ type="text"
530
+ value={input}
531
+ onChange={(e) => setInput(e.target.value)}
532
+ placeholder={
533
+ isGenerating
534
+ ? "Waiting for response..."
535
+ : "Type your message..."
536
+ }
537
+ disabled={isGenerating}
538
+ 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 ${
539
+ inputHasSecrets.length > 0
540
+ ? "border-yellow-600/50"
541
+ : "border-border"
542
+ }`}
543
+ />
544
+ </div>
545
+ <button
546
+ type="button"
547
+ className="p-2.5 rounded-lg hover:bg-background border border-border"
548
+ title="Voice input"
549
+ >
550
+ <Mic className="w-4 h-4 text-muted-foreground" />
551
+ </button>
552
+ <button
553
+ type="submit"
554
+ disabled={!input.trim() || isGenerating}
555
+ className="p-2.5 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
556
+ >
557
+ {isGenerating ? (
558
+ <Loader2 className="w-4 h-4 animate-spin" />
559
+ ) : (
265
560
  <Send className="w-4 h-4" />
266
- </button>
267
- </form>
268
- </footer>
269
- </div>
561
+ )}
562
+ </button>
563
+ </form>
564
+ </footer>
270
565
  </div>
271
566
  </DashboardLayout>
272
567
  );