@benzsiangco/jarvis 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/cli.js +478 -347
  2. package/dist/electron/main.js +160 -0
  3. package/dist/electron/preload.js +19 -0
  4. package/package.json +19 -6
  5. package/skills.md +147 -0
  6. package/src/agents/index.ts +248 -0
  7. package/src/brain/loader.ts +136 -0
  8. package/src/cli.ts +411 -0
  9. package/src/config/index.ts +363 -0
  10. package/src/core/executor.ts +222 -0
  11. package/src/core/plugins.ts +148 -0
  12. package/src/core/types.ts +217 -0
  13. package/src/electron/main.ts +192 -0
  14. package/src/electron/preload.ts +25 -0
  15. package/src/electron/types.d.ts +20 -0
  16. package/src/index.ts +12 -0
  17. package/src/providers/antigravity-loader.ts +233 -0
  18. package/src/providers/antigravity.ts +585 -0
  19. package/src/providers/index.ts +523 -0
  20. package/src/sessions/index.ts +194 -0
  21. package/src/tools/index.ts +436 -0
  22. package/src/tui/index.tsx +784 -0
  23. package/src/utils/auth-prompt.ts +394 -0
  24. package/src/utils/index.ts +180 -0
  25. package/src/utils/native-picker.ts +71 -0
  26. package/src/utils/skills.ts +99 -0
  27. package/src/utils/table-integration-examples.ts +617 -0
  28. package/src/utils/table-utils.ts +401 -0
  29. package/src/web/build-ui.ts +27 -0
  30. package/src/web/server.ts +674 -0
  31. package/src/web/ui/dist/.gitkeep +0 -0
  32. package/src/web/ui/dist/main.css +1 -0
  33. package/src/web/ui/dist/main.js +320 -0
  34. package/src/web/ui/dist/main.js.map +20 -0
  35. package/src/web/ui/index.html +46 -0
  36. package/src/web/ui/src/App.tsx +143 -0
  37. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  38. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  39. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  40. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  41. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  42. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  43. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  44. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  45. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  46. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  47. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  48. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  49. package/src/web/ui/src/config/models.ts +70 -0
  50. package/src/web/ui/src/main.tsx +13 -0
  51. package/src/web/ui/src/store/agentStore.ts +41 -0
  52. package/src/web/ui/src/store/uiStore.ts +64 -0
  53. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,620 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Bot, Paperclip, ArrowUp, RefreshCw, Zap, Layout, Search, Code, Activity, ChevronDown, ChevronUp, Square, X as XIcon } from 'lucide-react';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { MessageItem } from './MessageItem';
7
+ import type { Message } from '../../../types';
8
+ import { useAgentStore } from '../../../store/agentStore';
9
+
10
+ const JarvisLogo = () => (
11
+ <div className="relative group cursor-pointer flex items-center justify-center -mb-4">
12
+ <div className="absolute inset-0 bg-cyan-500/10 rounded-full blur-[80px] group-hover:bg-cyan-500/20 transition-all duration-1000" />
13
+
14
+ {/* Outer Rings */}
15
+ <div className="relative w-24 h-24 md:w-32 md:h-32 rounded-full border border-cyan-900/30 flex items-center justify-center animate-[spin_12s_linear_infinite]">
16
+ <div className="absolute top-0 w-1 h-1 bg-cyan-500 shadow-[0_0_15px_#06b6d4] rounded-full" />
17
+ <div className="absolute bottom-0 w-1 h-1 bg-cyan-500 shadow-[0_0_15px_#06b6d4] rounded-full" />
18
+ </div>
19
+ <div className="absolute w-18 h-18 md:w-24 md:h-24 rounded-full border border-dashed border-cyan-800/40 animate-[spin_20s_linear_infinite_reverse]" />
20
+
21
+ {/* Inner Core */}
22
+ <div className="absolute w-12 h-12 md:w-16 md:h-16 rounded-full bg-black/40 border-2 border-cyan-500/30 flex items-center justify-center shadow-[0_0_50px_rgba(6,182,212,0.2)] backdrop-blur-sm group-hover:scale-105 transition-transform duration-500">
23
+ <Bot size={20} className="md:w-8 md:h-8 text-cyan-400 drop-shadow-[0_0_15px_rgba(34,211,238,0.8)]" />
24
+ </div>
25
+ </div>
26
+ );
27
+
28
+ interface ChatStageProps {
29
+ sessionId: string | null;
30
+ onSessionCreated?: (id: string) => void;
31
+ }
32
+
33
+ const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
34
+
35
+ export function ChatStage({ sessionId, onSessionCreated }: ChatStageProps) {
36
+ const { selectedModelId, selectedAgentId, variant, setVariant } = useAgentStore();
37
+ const [messages, setMessages] = useState<Message[]>([]);
38
+ const [inputValue, setInputValue] = useState('');
39
+ const [isLoading, setIsLoading] = useState(false);
40
+ const [streamingText, setStreamingText] = useState('');
41
+ const [streamingThinking, setStreamingThinking] = useState('');
42
+ const [streamingToolCalls, setStreamingToolCalls] = useState<any[]>([]);
43
+ const [attachments, setAttachments] = useState<any[]>([]);
44
+ const [activeReader, setActiveReader] = useState<ReadableStreamDefaultReader<Uint8Array> | null>(null);
45
+ const [promptHistory, setPromptHistory] = useState<string[]>([]);
46
+ const [historyIndex, setHistoryIndex] = useState(-1);
47
+
48
+ const fileInputRef = useRef<HTMLInputElement>(null);
49
+ const scrollRef = useRef<HTMLDivElement>(null);
50
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
51
+
52
+ const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
53
+ if (e.target.files && e.target.files.length > 0) {
54
+ const newAttachments: any[] = [];
55
+ Array.from(e.target.files).forEach(file => {
56
+ newAttachments.push({
57
+ name: file.name,
58
+ type: file.type,
59
+ size: file.size,
60
+ file: file // Store actual file object for potential upload logic if needed, or read content
61
+ });
62
+ });
63
+ setAttachments(prev => [...prev, ...newAttachments]);
64
+ }
65
+ // Reset input so same file can be selected again
66
+ if (fileInputRef.current) fileInputRef.current.value = '';
67
+ };
68
+
69
+ const handlePaste = (e: React.ClipboardEvent) => {
70
+ const items = e.clipboardData?.items;
71
+ if (!items) return;
72
+
73
+ const files: File[] = [];
74
+ for (let i = 0; i < items.length; i++) {
75
+ const item = items[i];
76
+ if (item && item.kind === 'file') {
77
+ const file = item.getAsFile();
78
+ if (file) files.push(file);
79
+ }
80
+ }
81
+
82
+ if (files.length > 0) {
83
+ const newAttachments = files.map(file => ({
84
+ name: file.name,
85
+ type: file.type,
86
+ size: file.size,
87
+ file: file
88
+ }));
89
+ setAttachments(prev => [...prev, ...newAttachments]);
90
+ }
91
+ };
92
+
93
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
94
+ // Enter without shift sends message
95
+ if (e.key === 'Enter' && !e.shiftKey) {
96
+ e.preventDefault();
97
+ handleSendMessage();
98
+ return;
99
+ }
100
+
101
+ // Up arrow - navigate to previous prompt
102
+ if (e.key === 'ArrowUp' && !e.shiftKey && inputValue === '' && promptHistory.length > 0) {
103
+ e.preventDefault();
104
+ const newIndex = historyIndex === -1 ? promptHistory.length - 1 : Math.max(0, historyIndex - 1);
105
+ setHistoryIndex(newIndex);
106
+ setInputValue(promptHistory[newIndex] || '');
107
+ }
108
+
109
+ // Down arrow - navigate to next prompt
110
+ if (e.key === 'ArrowDown' && !e.shiftKey && historyIndex !== -1) {
111
+ e.preventDefault();
112
+ const newIndex = historyIndex + 1;
113
+ if (newIndex >= promptHistory.length) {
114
+ setHistoryIndex(-1);
115
+ setInputValue('');
116
+ } else {
117
+ setHistoryIndex(newIndex);
118
+ setInputValue(promptHistory[newIndex] || '');
119
+ }
120
+ }
121
+ };
122
+
123
+ const handleDrop = (e: React.DragEvent) => {
124
+ e.preventDefault();
125
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
126
+ const newAttachments: any[] = [];
127
+ Array.from(e.dataTransfer.files).forEach(file => {
128
+ newAttachments.push({
129
+ name: file.name,
130
+ type: file.type,
131
+ size: file.size,
132
+ file: file
133
+ });
134
+ });
135
+ setAttachments(prev => [...prev, ...newAttachments]);
136
+ }
137
+ };
138
+
139
+ const removeAttachment = (index: number) => {
140
+ setAttachments(prev => prev.filter((_, i) => i !== index));
141
+ };
142
+
143
+ useEffect(() => {
144
+ if (sessionId) {
145
+ fetch(`/api/sessions/${sessionId}`).then(r => r.json()).then(data => {
146
+ setMessages(data.messages || []);
147
+ });
148
+ } else {
149
+ setMessages([]);
150
+ }
151
+ }, [sessionId]);
152
+
153
+ useEffect(() => {
154
+ if (scrollRef.current) {
155
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
156
+ }
157
+ }, [messages, streamingText, streamingThinking, streamingToolCalls]);
158
+
159
+ const handleCancel = async () => {
160
+ if (!sessionId) return;
161
+ try {
162
+ await fetch('/api/chat/cancel', {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ sessionId })
166
+ });
167
+ if (activeReader) {
168
+ await activeReader.cancel();
169
+ setActiveReader(null);
170
+ }
171
+ setIsLoading(false);
172
+ } catch (e) {
173
+ console.error('Failed to cancel:', e);
174
+ }
175
+ };
176
+
177
+ const handleSendMessage = async (text: string = inputValue) => {
178
+ if ((!text.trim() && attachments.length === 0) || isLoading) return;
179
+
180
+ // Add to history
181
+ if (text.trim()) {
182
+ setPromptHistory(prev => [...prev, text.trim()]);
183
+ setHistoryIndex(-1);
184
+ }
185
+
186
+ let activeSessionId = sessionId;
187
+
188
+ // Create session if none exists
189
+ if (!activeSessionId) {
190
+ try {
191
+ const res = await fetch('/api/sessions', {
192
+ method: 'POST',
193
+ headers: { 'Content-Type': 'application/json' },
194
+ body: JSON.stringify({ agentId: 'build' })
195
+ });
196
+ const data = await res.json();
197
+ activeSessionId = data.id;
198
+ // Notify parent to update URL/State, but continue locally
199
+ if (onSessionCreated) onSessionCreated(data.id);
200
+ } catch (e) {
201
+ console.error('Failed to create session', e);
202
+ return;
203
+ }
204
+ }
205
+
206
+ // Process attachments to send as Base64
207
+ const processedAttachments = await Promise.all(attachments.map(async (a) => {
208
+ if (!a.file) return a;
209
+ return new Promise((resolve) => {
210
+ const reader = new FileReader();
211
+ reader.onloadend = () => {
212
+ resolve({
213
+ name: a.name,
214
+ type: a.type,
215
+ size: a.size,
216
+ content: reader.result // This will be the data URL (Base64)
217
+ });
218
+ };
219
+ reader.readAsDataURL(a.file);
220
+ });
221
+ }));
222
+
223
+ const userMsg: Message = {
224
+ id: 'msg-' + Date.now(),
225
+ role: 'user',
226
+ content: text,
227
+ timestamp: new Date() as any,
228
+ attachments: attachments // Keep original for local UI display
229
+ };
230
+
231
+ setMessages(prev => [...prev, userMsg]);
232
+ setInputValue('');
233
+ setAttachments([]); // Clear attachments after sending
234
+ if (textareaRef.current) textareaRef.current.style.height = 'auto';
235
+
236
+ setIsLoading(true);
237
+ setStreamingText('');
238
+ setStreamingThinking('');
239
+ setStreamingToolCalls([]);
240
+
241
+ try {
242
+ const response = await fetch('/api/chat', {
243
+ method: 'POST',
244
+ headers: { 'Content-Type': 'application/json' },
245
+ body: JSON.stringify({
246
+ sessionId: activeSessionId,
247
+ message: text,
248
+ agentId: selectedAgentId,
249
+ modelId: selectedModelId,
250
+ variant,
251
+ attachments: processedAttachments // Send Base64 attachments to backend
252
+ })
253
+ });
254
+
255
+ const reader = response.body?.getReader();
256
+ if (reader) setActiveReader(reader);
257
+ const decoder = new TextDecoder();
258
+
259
+ let accumulatedContent = '';
260
+ let accumulatedThinking = '';
261
+ let accumulatedToolCalls: any[] = [];
262
+ let buffer = '';
263
+
264
+ if (reader) {
265
+ while (true) {
266
+ const { value, done } = await reader.read();
267
+ if (done) break;
268
+ buffer += decoder.decode(value, { stream: true });
269
+
270
+ const tagRegex = /<<<(THOUGHT|END_THOUGHT|TOOL_START|TOOL_END):?([\s\S]*?)?>>>/g;
271
+ let match;
272
+ let currentToolCalls: any[] = [];
273
+
274
+ while ((match = tagRegex.exec(buffer)) !== null) {
275
+ const tagType = match[1];
276
+ const tagValue = match[2] || '';
277
+
278
+ if (tagType === 'TOOL_START') {
279
+ try {
280
+ const toolData = JSON.parse(tagValue);
281
+ currentToolCalls.push({ name: toolData.name, args: toolData.args, status: 'running' });
282
+ } catch (e) {}
283
+ } else if (tagType === 'TOOL_END') {
284
+ try {
285
+ const toolData = JSON.parse(tagValue);
286
+ const idx = currentToolCalls.findIndex(t => t.name === toolData.name && t.status === 'running');
287
+ if (idx !== -1) {
288
+ currentToolCalls[idx] = { ...currentToolCalls[idx], result: toolData.result, status: 'completed' };
289
+ } else {
290
+ currentToolCalls.push({ name: toolData.name, result: toolData.result, status: 'completed' });
291
+ }
292
+ } catch (e) {}
293
+ }
294
+ }
295
+
296
+ // Thinking extraction
297
+ let currentThinking = '';
298
+ const thinkingRegex = /<<<THOUGHT>>>([\s\S]*?)(?:<<<END_THOUGHT>>>|$)/g;
299
+ let tMatch;
300
+ while ((tMatch = thinkingRegex.exec(buffer)) !== null) {
301
+ currentThinking += tMatch[1];
302
+ }
303
+
304
+ // Clean content removal
305
+ // 1. Remove thinking blocks entirely
306
+ let cleanContent = buffer.replace(/<<<THOUGHT>>>[\s\S]*?(?:<<<END_THOUGHT>>>|$)/g, '');
307
+ // 2. Remove all other tags
308
+ cleanContent = cleanContent.replace(/<<<(?:THOUGHT|END_THOUGHT|TOOL_START|TOOL_END):?[\s\S]*?>>>/g, '');
309
+ // 3. Remove partial tags at the end of the buffer to prevent them from flashing
310
+ cleanContent = cleanContent.replace(/<<<[A-Z_]*:?[\s\S]*?$/g, '');
311
+
312
+ accumulatedContent = cleanContent;
313
+ accumulatedThinking = currentThinking;
314
+ accumulatedToolCalls = currentToolCalls;
315
+
316
+ setStreamingText(accumulatedContent);
317
+ setStreamingThinking(accumulatedThinking);
318
+ setStreamingToolCalls(accumulatedToolCalls);
319
+ }
320
+ }
321
+
322
+ setMessages(prev => [...prev, {
323
+ id: 'msg-' + Date.now(),
324
+ role: 'assistant',
325
+ content: accumulatedContent,
326
+ thinking: accumulatedThinking,
327
+ toolCalls: accumulatedToolCalls as any,
328
+ timestamp: new Date() as any
329
+ }]);
330
+ setStreamingText(''); setStreamingThinking(''); setStreamingToolCalls([]);
331
+ setActiveReader(null);
332
+ } catch (e) {
333
+ console.error(e);
334
+ } finally {
335
+ setIsLoading(false);
336
+ setActiveReader(null);
337
+ }
338
+ };
339
+
340
+ return (
341
+ <div className="flex-1 flex flex-col min-h-0 relative h-full">
342
+ <div ref={scrollRef} className="flex-1 overflow-y-auto custom-scrollbar px-4 pt-10">
343
+ {messages.length === 0 && !streamingText ? (
344
+ <div className="h-full flex flex-col items-center justify-center space-y-4 md:space-y-6 pt-10 pb-20 max-w-5xl mx-auto px-6">
345
+ <JarvisLogo />
346
+
347
+ <div className="text-center space-y-1 relative w-full">
348
+ <div className="absolute -inset-10 bg-cyan-500/5 blur-3xl rounded-full pointer-events-none" />
349
+ <h1 className="text-3xl md:text-5xl font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-cyan-200 tracking-widest uppercase italic drop-shadow-[0_0_25px_rgba(6,182,212,0.6)] font-mono break-words">
350
+ J.A.R.V.I.S.
351
+ </h1>
352
+ <div className="flex items-center justify-center gap-3 text-cyan-500/80">
353
+ <div className="h-px w-6 md:w-8 bg-cyan-800" />
354
+ <p className="text-[7px] md:text-[8px] font-bold uppercase tracking-[0.2em] md:tracking-[0.3em] text-cyan-400 text-center">Just A Rather Very Intelligent System</p>
355
+ <div className="h-px w-6 md:w-8 bg-cyan-800" />
356
+ </div>
357
+ </div>
358
+
359
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 md:gap-4 w-full md:px-4 max-w-md md:max-w-none">
360
+ {[
361
+ { t: 'Software Architecture', d: 'Design a system with Docker & Redis', i: Layout, color: 'text-cyan-400', border: 'hover:border-cyan-500/50', glow: 'group-hover:shadow-[0_0_20px_rgba(34,211,238,0.2)]' },
362
+ { t: 'Code Review', d: 'Optimize this React component', i: Search, color: 'text-emerald-400', border: 'hover:border-emerald-500/50', glow: 'group-hover:shadow-[0_0_20px_rgba(52,211,153,0.2)]' },
363
+ { t: 'Data Structures', d: 'Explain B-Trees vs Red-Black Trees', i: Code, color: 'text-amber-400', border: 'hover:border-amber-500/50', glow: 'group-hover:shadow-[0_0_20px_rgba(251,191,36,0.2)]' },
364
+ { t: 'DevOps Flow', d: 'Setup CI/CD for my Node project', i: Activity, color: 'text-indigo-400', border: 'hover:border-indigo-500/50', glow: 'group-hover:shadow-[0_0_20px_rgba(129,140,248,0.2)]' }
365
+ ].map(q => (
366
+ <button
367
+ key={q.t}
368
+ onClick={() => handleSendMessage(q.d)}
369
+ className={cn(
370
+ "p-4 md:p-5 bg-zinc-950/40 border border-zinc-800/60 rounded-lg text-left transition-all duration-300 group relative overflow-hidden backdrop-blur-sm",
371
+ q.border,
372
+ q.glow
373
+ )}
374
+ >
375
+ <div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
376
+ <div className="relative z-10 flex xl:flex-col items-center xl:items-start gap-4 xl:gap-3">
377
+ <div className={cn("p-2.5 rounded-lg bg-zinc-900/80 border border-zinc-800 group-hover:scale-110 transition-transform duration-300 shadow-inner shrink-0", q.color)}>
378
+ <q.i size={20} />
379
+ </div>
380
+ <div className="min-w-0">
381
+ <p className="text-xs md:text-sm font-black text-zinc-100 uppercase tracking-wide group-hover:text-white transition-colors truncate">{q.t}</p>
382
+ <p className="text-[10px] md:text-xs text-zinc-500 mt-0.5 font-medium opacity-60 group-hover:opacity-100 transition-opacity truncate xl:whitespace-normal xl:line-clamp-2">{q.d}</p>
383
+ </div>
384
+ </div>
385
+ </button>
386
+ ))}
387
+ </div>
388
+ </div>
389
+ ) : (
390
+ <div className="flex flex-col pb-64">
391
+ {messages.map(m => <MessageItem key={m.id} message={m} />)}
392
+
393
+ <AnimatePresence>
394
+ {/* Streaming thinking is now shown inline with the message */}
395
+ </AnimatePresence>
396
+
397
+ {streamingText || streamingToolCalls.length > 0 || streamingThinking ? (
398
+ <div className="w-full py-6 border-b border-zinc-900/30 bg-zinc-900/5">
399
+ <div className="max-w-4xl mx-auto flex gap-6 px-6 md:px-12">
400
+ <div className="w-8 h-8 rounded shrink-0 flex items-center justify-center shadow-lg bg-cyan-900/20 text-cyan-500">
401
+ <Bot size={16} />
402
+ </div>
403
+ <div className="flex-1 min-w-0 space-y-4">
404
+ {streamingThinking && (
405
+ <div className="mb-4">
406
+ <div className="text-xs font-medium text-zinc-500 mb-2 flex items-center gap-2">
407
+ <span>Thinking</span>
408
+ <div className="w-1 h-1 rounded-full bg-zinc-500 animate-pulse"></div>
409
+ </div>
410
+ <div className="pl-3 border-l-2 border-amber-500/20 text-sm text-zinc-500 font-mono leading-relaxed">
411
+ {streamingThinking}
412
+ </div>
413
+ </div>
414
+ )}
415
+
416
+ {streamingToolCalls.length > 0 && (
417
+ <div className="space-y-3">
418
+ {streamingToolCalls.map((t, i) => {
419
+ const isEdit = t.name === 'edit';
420
+ const isBash = t.name === 'bash';
421
+
422
+ return (
423
+ <div key={i} className="border border-zinc-800 rounded-xl bg-zinc-900/50 overflow-hidden shadow-lg">
424
+ {/* Tool Header */}
425
+ <div className="flex items-center justify-between px-4 py-3 bg-zinc-900/80 border-b border-zinc-800">
426
+ <div className="flex items-center gap-3">
427
+ <div className="p-2 rounded-lg bg-zinc-950 text-cyan-500">
428
+ <Activity size={14} />
429
+ </div>
430
+ <div className="text-left">
431
+ <p className="text-[10px] font-black uppercase tracking-widest text-zinc-500">Running</p>
432
+ <p className="text-sm font-bold text-zinc-100">{t.name === 'bash' ? 'Shell' : t.name}</p>
433
+ </div>
434
+ </div>
435
+ <span className="text-[9px] font-black uppercase tracking-widest text-cyan-500 animate-pulse flex items-center gap-2">
436
+ <div className="w-1.5 h-1.5 rounded-full bg-cyan-500 animate-ping"></div>
437
+ Live
438
+ </span>
439
+ </div>
440
+
441
+ {/* Tool Content */}
442
+ <div className="p-4 space-y-3">
443
+ {isEdit && t.args?.filePath && (
444
+ <>
445
+ <div className="space-y-1">
446
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">File</div>
447
+ <div className="font-mono text-xs text-zinc-400 bg-zinc-950 p-2 rounded-lg border border-zinc-800">
448
+ {t.args.filePath}
449
+ </div>
450
+ </div>
451
+
452
+ {t.args.oldString && (
453
+ <div className="space-y-1">
454
+ <div className="text-[10px] font-black uppercase text-red-400 tracking-wider flex items-center gap-2">
455
+ <span>Removing</span>
456
+ <span className="font-mono text-[9px]">{t.args.oldString.split('\n').length} lines</span>
457
+ </div>
458
+ <div className="font-mono text-xs text-red-400 bg-red-950/20 p-3 rounded-lg border border-red-900/30 max-h-40 overflow-y-auto">
459
+ {t.args.oldString}
460
+ </div>
461
+ </div>
462
+ )}
463
+
464
+ {t.args.newString && (
465
+ <div className="space-y-1">
466
+ <div className="text-[10px] font-black uppercase text-emerald-400 tracking-wider flex items-center gap-2">
467
+ <span>Adding</span>
468
+ <span className="font-mono text-[9px]">{t.args.newString.split('\n').length} lines</span>
469
+ </div>
470
+ <div className="font-mono text-xs text-emerald-400 bg-emerald-950/20 p-3 rounded-lg border border-emerald-900/30 max-h-40 overflow-y-auto">
471
+ {t.args.newString}
472
+ </div>
473
+ </div>
474
+ )}
475
+ </>
476
+ )}
477
+
478
+ {isBash && t.args?.command && (
479
+ <div className="space-y-1">
480
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">Command</div>
481
+ <div className="font-mono text-xs text-emerald-400 bg-zinc-950 p-3 rounded-lg border border-zinc-800">
482
+ {t.args.command}
483
+ </div>
484
+ </div>
485
+ )}
486
+
487
+ {!isEdit && !isBash && (
488
+ <div className="space-y-1">
489
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">Details</div>
490
+ <div className="font-mono text-xs text-zinc-400 bg-zinc-950 p-3 rounded-lg border border-zinc-800">
491
+ {JSON.stringify(t.args, null, 2)}
492
+ </div>
493
+ </div>
494
+ )}
495
+ </div>
496
+ </div>
497
+ );
498
+ })}
499
+ </div>
500
+ )}
501
+
502
+ {streamingText && (
503
+ <div className="prose prose-invert prose-zinc max-w-none text-base leading-relaxed prose-headings:font-bold prose-p:text-zinc-300 prose-pre:bg-transparent prose-pre:p-0 prose-pre:m-0">
504
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{streamingText}</ReactMarkdown>
505
+ </div>
506
+ )}
507
+ </div>
508
+ </div>
509
+ </div>
510
+ ) : null}
511
+ </div>
512
+ )}
513
+ </div>
514
+
515
+ {/* Input Stage */}
516
+ <div className="absolute bottom-0 left-0 right-0 p-4 md:p-10 bg-gradient-to-t from-zinc-950 via-zinc-950/90 to-transparent pointer-events-none z-30">
517
+ <div className="max-w-4xl mx-auto relative pointer-events-auto">
518
+ {/* Variant Selector */}
519
+ <div className="flex justify-center mb-4 gap-2">
520
+ {[
521
+ { id: 'standard', label: 'Standard', color: 'text-zinc-400', active: 'bg-zinc-800 text-zinc-100 ring-1 ring-white/10' },
522
+ { id: 'creative', label: 'Creative', color: 'text-purple-400', active: 'bg-purple-900/20 text-purple-400 ring-1 ring-purple-500/30' },
523
+ { id: 'precise', label: 'Precise', color: 'text-emerald-400', active: 'bg-emerald-900/20 text-emerald-400 ring-1 ring-emerald-500/30' }
524
+ ].map(v => (
525
+ <button
526
+ key={v.id}
527
+ onClick={() => setVariant(v.id as any)}
528
+ className={cn(
529
+ "px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest transition-all",
530
+ variant === v.id ? v.active : cn("text-zinc-600 hover:text-zinc-400", v.color.replace('text-', 'hover:text-'))
531
+ )}
532
+ >
533
+ {v.label}
534
+ </button>
535
+ ))}
536
+ </div>
537
+
538
+ {attachments.length > 0 && (
539
+ <div className="flex flex-wrap gap-2 mb-3 px-1">
540
+ {attachments.map((file, i) => (
541
+ <div key={i} className="relative group">
542
+ <div className="flex items-center gap-2 bg-zinc-900/50 border border-zinc-800 rounded-lg px-2 py-1.5 pr-7">
543
+ <div className="w-6 h-6 rounded bg-zinc-800/50 flex items-center justify-center text-zinc-500 shrink-0">
544
+ <Paperclip size={12} />
545
+ </div>
546
+ <span className="text-xs font-medium text-zinc-400 truncate max-w-[100px]">{file.name}</span>
547
+ </div>
548
+ <button
549
+ onClick={() => removeAttachment(i)}
550
+ className="absolute -top-1 -right-1 p-0.5 bg-zinc-900 border border-zinc-800 rounded-full text-zinc-500 hover:text-red-400 hover:border-red-800 transition-colors opacity-0 group-hover:opacity-100"
551
+ >
552
+ <XIcon size={10} />
553
+ </button>
554
+ </div>
555
+ ))}
556
+ </div>
557
+ )}
558
+
559
+ <form
560
+ onSubmit={(e) => { e.preventDefault(); handleSendMessage(); }}
561
+ onDragOver={(e) => e.preventDefault()}
562
+ onDrop={handleDrop}
563
+ className="relative bg-zinc-900/90 backdrop-blur-3xl border border-zinc-800 rounded-xl md:rounded-[2rem] p-2 md:p-3 shadow-[0_20px_50px_rgba(0,0,0,0.6)] focus-within:border-zinc-700 transition-all ring-1 ring-white/5"
564
+ >
565
+ <div className="flex items-end gap-2 md:gap-3 px-1 md:px-2">
566
+ <input
567
+ type="file"
568
+ multiple
569
+ ref={fileInputRef}
570
+ className="hidden"
571
+ onChange={handleFileSelect}
572
+ />
573
+ <button
574
+ type="button"
575
+ onClick={() => fileInputRef.current?.click()}
576
+ className="p-3 md:p-4 text-zinc-500 hover:text-cyan-500 transition-all active:scale-90 bg-zinc-950 rounded-lg md:rounded-2xl"
577
+ >
578
+ <Paperclip size={18} className="md:w-[22px] md:h-[22px]" />
579
+ </button>
580
+
581
+ <textarea
582
+ ref={textareaRef}
583
+ rows={1}
584
+ autoFocus
585
+ value={inputValue}
586
+ onChange={(e) => {
587
+ setInputValue(e.target.value);
588
+ e.target.style.height = 'auto';
589
+ e.target.style.height = `${Math.min(e.target.scrollHeight, 250)}px`;
590
+ }}
591
+ onKeyDown={handleKeyDown}
592
+ onPaste={handlePaste}
593
+ placeholder="Message JARVIS..."
594
+ className="flex-1 bg-transparent border-none py-3 md:py-4 px-2 text-zinc-100 focus:outline-none focus:ring-0 text-base md:text-[18px] resize-none max-h-40 md:max-h-60 placeholder-zinc-700 leading-normal custom-scrollbar font-medium"
595
+ />
596
+
597
+ <button
598
+ type={isLoading ? "button" : "submit"}
599
+ onClick={isLoading ? handleCancel : undefined}
600
+ disabled={!isLoading && (!inputValue.trim() && attachments.length === 0)}
601
+ className={cn(
602
+ "p-3 md:p-4 rounded-lg md:rounded-2xl transition-all active:scale-90 flex items-center justify-center min-w-[48px] md:min-w-[64px]",
603
+ (inputValue.trim() || attachments.length > 0 || isLoading) ? "bg-white text-black shadow-xl" : "bg-zinc-800 text-zinc-600"
604
+ )}
605
+ >
606
+ {isLoading ? <Square size={18} fill="currentColor" className="md:w-6 md:h-6" /> : <ArrowUp size={18} strokeWidth={3} className="md:w-6 md:h-6" />}
607
+ </button>
608
+ </div>
609
+ </form>
610
+
611
+ <div className="hidden flex justify-center gap-4 md:gap-8 mt-4 text-[8px] md:text-[9px] font-black uppercase tracking-[0.2em] md:tracking-[0.4em] text-zinc-700 italic opacity-50 md:opacity-100">
612
+ <span className="hidden md:inline">Secure Link Active</span>
613
+ <span>OS 1.0.3-B</span>
614
+ <span className="hidden md:inline">Workspace: Synchronized</span>
615
+ </div>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ );
620
+ }