@benzsiangco/jarvis 1.0.0 → 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 (55) hide show
  1. package/README.md +5 -0
  2. package/bin/{jarvis.js → jarvis} +1 -1
  3. package/dist/cli.js +476 -350
  4. package/dist/electron/main.js +160 -0
  5. package/dist/electron/preload.js +19 -0
  6. package/package.json +21 -8
  7. package/skills.md +147 -0
  8. package/src/agents/index.ts +248 -0
  9. package/src/brain/loader.ts +136 -0
  10. package/src/cli.ts +411 -0
  11. package/src/config/index.ts +363 -0
  12. package/src/core/executor.ts +222 -0
  13. package/src/core/plugins.ts +148 -0
  14. package/src/core/types.ts +217 -0
  15. package/src/electron/main.ts +192 -0
  16. package/src/electron/preload.ts +25 -0
  17. package/src/electron/types.d.ts +20 -0
  18. package/src/index.ts +12 -0
  19. package/src/providers/antigravity-loader.ts +233 -0
  20. package/src/providers/antigravity.ts +585 -0
  21. package/src/providers/index.ts +523 -0
  22. package/src/sessions/index.ts +194 -0
  23. package/src/tools/index.ts +436 -0
  24. package/src/tui/index.tsx +784 -0
  25. package/src/utils/auth-prompt.ts +394 -0
  26. package/src/utils/index.ts +180 -0
  27. package/src/utils/native-picker.ts +71 -0
  28. package/src/utils/skills.ts +99 -0
  29. package/src/utils/table-integration-examples.ts +617 -0
  30. package/src/utils/table-utils.ts +401 -0
  31. package/src/web/build-ui.ts +27 -0
  32. package/src/web/server.ts +674 -0
  33. package/src/web/ui/dist/.gitkeep +0 -0
  34. package/src/web/ui/dist/main.css +1 -0
  35. package/src/web/ui/dist/main.js +320 -0
  36. package/src/web/ui/dist/main.js.map +20 -0
  37. package/src/web/ui/index.html +46 -0
  38. package/src/web/ui/src/App.tsx +143 -0
  39. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  40. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  41. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  42. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  43. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  44. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  45. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  46. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  47. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  48. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  49. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  50. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  51. package/src/web/ui/src/config/models.ts +70 -0
  52. package/src/web/ui/src/main.tsx +13 -0
  53. package/src/web/ui/src/store/agentStore.ts +41 -0
  54. package/src/web/ui/src/store/uiStore.ts +64 -0
  55. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,446 @@
1
+ import React, { useState } from 'react';
2
+ import ReactMarkdown from 'react-markdown';
3
+ import remarkGfm from 'remark-gfm';
4
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
6
+ import { User, Bot, Copy, Check, Zap, ChevronDown, ChevronUp, Terminal as LucideTerminal, Code, Activity, Link, Sliders, FileText, Play, AlertCircle } from 'lucide-react';
7
+ import type { Message, ToolCall } from '../../../types';
8
+
9
+ const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
10
+
11
+ // Helper to get language from file extension
12
+ const getLanguageFromFilePath = (filePath: string): string => {
13
+ if (!filePath) return 'text';
14
+ const ext = filePath.split('.').pop()?.toLowerCase();
15
+ const langMap: Record<string, string> = {
16
+ 'js': 'javascript',
17
+ 'jsx': 'jsx',
18
+ 'ts': 'typescript',
19
+ 'tsx': 'tsx',
20
+ 'py': 'python',
21
+ 'html': 'html',
22
+ 'css': 'css',
23
+ 'json': 'json',
24
+ 'md': 'markdown',
25
+ 'sh': 'bash',
26
+ 'yml': 'yaml',
27
+ 'yaml': 'yaml',
28
+ };
29
+ return langMap[ext || ''] || 'text';
30
+ };
31
+
32
+ // Helper to format tool args in a readable way
33
+ const formatToolArgs = (tool: ToolCall) => {
34
+ if (tool.name === 'bash') {
35
+ // Always show the actual command
36
+ return tool.args.command || tool.args.description || '';
37
+ }
38
+
39
+ if (tool.name === 'read') {
40
+ return `File: ${tool.args.filePath || 'unknown'}`;
41
+ }
42
+
43
+ if (tool.name === 'write') {
44
+ const lines = tool.args.content ? tool.args.content.split('\n').length : 0;
45
+ return `File: ${tool.args.filePath || 'unknown'}\nLines: ${lines}`;
46
+ }
47
+
48
+ if (tool.name === 'edit') {
49
+ // For edit, we'll show a custom diff view
50
+ return null; // Special case handled in display
51
+ }
52
+
53
+ return JSON.stringify(tool.args, null, 2);
54
+ };
55
+
56
+ // Helper to format tool result in a readable way
57
+ const formatToolResult = (tool: ToolCall) => {
58
+ const result = tool.result;
59
+
60
+ // Handle bash/shell output specially
61
+ if (tool.name === 'bash') {
62
+ if (typeof result === 'string') {
63
+ return result.trim();
64
+ }
65
+
66
+ if (result && typeof result === 'object') {
67
+ // Extract actual shell output from response object
68
+ if (result.error) {
69
+ return result.error;
70
+ }
71
+ if (result.content) {
72
+ return result.content;
73
+ }
74
+ // If there's stdout/stderr
75
+ if (result.stdout) {
76
+ return result.stderr ? `${result.stdout}\n${result.stderr}` : result.stdout;
77
+ }
78
+ }
79
+ }
80
+
81
+ // For other tools
82
+ if (typeof result === 'string') {
83
+ return result.trim();
84
+ }
85
+
86
+ if (result && typeof result === 'object') {
87
+ // Check if it's a response object with content
88
+ if (result.content && typeof result.content === 'string') {
89
+ return result.content;
90
+ }
91
+ // Check for error field
92
+ if (result.error && typeof result.error === 'string') {
93
+ return result.error;
94
+ }
95
+ return JSON.stringify(result, null, 2);
96
+ }
97
+
98
+ return String(result);
99
+ };
100
+
101
+ const markdownComponents = {
102
+ code({ node, inline, className, children, ...props }: any) {
103
+ const match = /language-(\w+)/.exec(className || '');
104
+ const codeString = String(children).replace(/\n$/, '');
105
+ const [copied, setCopied] = useState(false);
106
+
107
+ const handleCopy = () => {
108
+ navigator.clipboard.writeText(codeString);
109
+ setCopied(true);
110
+ setTimeout(() => setCopied(false), 2000);
111
+ };
112
+
113
+ return !inline && match ? (
114
+ <div className="my-6 rounded-xl overflow-hidden border border-zinc-800 bg-zinc-950 shadow-sm text-left group">
115
+ <div className="bg-zinc-900 px-4 py-2.5 border-b border-zinc-800 flex justify-between items-center">
116
+ <span className="text-[10px] font-black uppercase tracking-widest text-zinc-500">{match[1]}</span>
117
+ <button onClick={handleCopy} className="p-1.5 hover:bg-zinc-800 rounded-md text-zinc-500 hover:text-zinc-200 transition-all active:scale-90">
118
+ {copied ? <Check size={14} className="text-emerald-500" /> : <Copy size={14} />}
119
+ </button>
120
+ </div>
121
+ <SyntaxHighlighter
122
+ style={vscDarkPlus}
123
+ language={match[1]}
124
+ PreTag="div"
125
+ customStyle={{
126
+ margin: 0,
127
+ padding: '1.25rem',
128
+ background: 'transparent',
129
+ fontSize: '0.875rem',
130
+ lineHeight: '1.6'
131
+ }}
132
+ >
133
+ {codeString}
134
+ </SyntaxHighlighter>
135
+ </div>
136
+ ) : (
137
+ <code className="bg-zinc-800/80 text-zinc-200 px-1.5 py-0.5 rounded font-mono text-sm border border-zinc-700/30" {...props}>
138
+ {children}
139
+ </code>
140
+ );
141
+ }
142
+ };
143
+
144
+ function ToolSteps({ tools }: { tools: ToolCall[] }) {
145
+ const [isExpanded, setIsExpanded] = useState(true);
146
+
147
+ // Mock duration for now as backend doesn't provide tool-level timing yet
148
+ const duration = "2s";
149
+
150
+ return (
151
+ <div className="mb-4">
152
+ <button
153
+ type="button"
154
+ onClick={() => setIsExpanded(!isExpanded)}
155
+ className="flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors select-none group"
156
+ >
157
+ <span>{isExpanded ? 'Hide steps' : 'Show steps'}</span>
158
+ <span className="opacity-50">·</span>
159
+ <span className="font-mono opacity-80">{tools.length} items</span>
160
+ <div className="flex items-center justify-center w-4 h-4 opacity-70 group-hover:opacity-100 transition-opacity ml-0.5">
161
+ <svg
162
+ fill="none"
163
+ viewBox="0 0 20 20"
164
+ aria-hidden="true"
165
+ className={cn("w-4 h-4 transition-transform duration-200", isExpanded && "rotate-180")}
166
+ >
167
+ <path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" strokeLinecap="square"></path>
168
+ </svg>
169
+ </div>
170
+ </button>
171
+
172
+ {isExpanded && (
173
+ <div className="mt-2 space-y-2 pl-1 border-l-2 border-zinc-800/50 ml-1">
174
+ {tools.map((t, i) => <ToolCallRow key={i} tool={t} />)}
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ }
180
+
181
+ function ToolCallRow({ tool }: { tool: ToolCall }) {
182
+ const [isOpen, setIsOpen] = useState(false);
183
+ const isDangerous = ['bash', 'edit'].includes(tool.name);
184
+ const isError = tool.result && (
185
+ (typeof tool.result === 'string' && (tool.result.startsWith('Error') || tool.result.includes('not found') || tool.result.includes('failed')))
186
+ );
187
+
188
+ // Icon Mapping
189
+ const Icon = tool.name === 'bash' ? LucideTerminal :
190
+ tool.name === 'read' ? Link :
191
+ tool.name === 'edit' ? Sliders :
192
+ tool.name === 'write' ? FileText : Play;
193
+
194
+ // Header Text
195
+ const summary = tool.name === 'bash' ? (tool.args.description || tool.args.command) :
196
+ tool.name === 'read' ? (tool.args.filePath?.split(/[/\\]/).pop() || 'file') :
197
+ tool.name === 'write' ? (tool.args.filePath?.split(/[/\\]/).pop() || 'file') :
198
+ tool.name === 'edit' ? (tool.args.filePath?.split(/[/\\]/).pop() || 'file') :
199
+ JSON.stringify(tool.args).slice(0, 50);
200
+
201
+ // Diff Stats for Edit
202
+ let diffStats = null;
203
+ if (tool.name === 'edit' && tool.args.newString && tool.args.oldString) {
204
+ const added = tool.args.newString.split('\n').length;
205
+ const removed = tool.args.oldString.split('\n').length;
206
+ diffStats = (
207
+ <div className="flex items-center gap-3 text-[10px] font-mono ml-auto mr-4">
208
+ <span className="text-emerald-500">+{added}</span>
209
+ <span className="text-red-500">-{removed}</span>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ // Special rendering for Edit tool
215
+ const isEdit = tool.name === 'edit';
216
+
217
+ return (
218
+ <div className="rounded-lg overflow-hidden transition-all duration-200">
219
+ <button
220
+ onClick={() => setIsOpen(!isOpen)}
221
+ className={cn(
222
+ "w-full flex items-center gap-4 px-4 py-3 text-left border rounded-xl transition-all group select-none",
223
+ isError
224
+ ? "bg-red-500/10 border-red-500/20 hover:bg-red-500/15"
225
+ : isOpen
226
+ ? "bg-zinc-800/40 border-zinc-700"
227
+ : "bg-zinc-900/40 border-zinc-800/60 hover:bg-zinc-800/40 hover:border-zinc-700/60"
228
+ )}
229
+ >
230
+ {/* Status / Icon */}
231
+ <div className={cn(
232
+ "shrink-0 flex items-center justify-center w-5 h-5",
233
+ isError ? "text-red-500" : "text-zinc-500"
234
+ )}>
235
+ {isError ? <AlertCircle size={16} /> : <Icon size={16} />}
236
+ </div>
237
+
238
+ {/* Label */}
239
+ <span className={cn(
240
+ "text-sm font-bold shrink-0 capitalize",
241
+ isError ? "text-red-400" : "text-zinc-200"
242
+ )}>
243
+ {tool.name === 'bash' ? 'Shell' : tool.name}
244
+ </span>
245
+
246
+ {/* Content Summary */}
247
+ <span className={cn(
248
+ "text-sm font-medium truncate flex-1",
249
+ isError ? "text-red-400/70" : "text-zinc-500"
250
+ )}>
251
+ {isError && typeof tool.result === 'string' ? tool.result.slice(0, 60) + '...' : summary}
252
+ </span>
253
+
254
+ {/* Diff Stats (if edit) */}
255
+ {diffStats}
256
+
257
+ {/* Chevron */}
258
+ <div className={cn("shrink-0 transition-transform duration-200 text-zinc-600", isOpen && "rotate-180")}>
259
+ <ChevronDown size={14} />
260
+ </div>
261
+ </button>
262
+
263
+ {isOpen && (
264
+ <div className="px-4 pb-3 pt-2 ml-4 border-l-2 border-zinc-800 space-y-3 mt-1">
265
+ {isEdit ? (
266
+ // Special Edit Display with Syntax Highlighting
267
+ <>
268
+ <div className="space-y-1">
269
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">File</div>
270
+ <div className="font-mono text-xs text-zinc-400 bg-zinc-950/50 p-2 rounded-lg border border-zinc-900">
271
+ {tool.args.filePath}
272
+ </div>
273
+ </div>
274
+
275
+ <div className="space-y-1">
276
+ <div className="text-[10px] font-black uppercase text-red-400 tracking-wider flex items-center gap-2">
277
+ <span>Removed</span>
278
+ <span className="font-mono text-[9px]">{tool.args.oldString?.split('\n').length || 0} lines</span>
279
+ </div>
280
+ <div className="rounded-lg overflow-hidden border border-red-900/30 bg-red-950/10">
281
+ <SyntaxHighlighter
282
+ language={getLanguageFromFilePath(tool.args.filePath)}
283
+ style={vscDarkPlus}
284
+ showLineNumbers={true}
285
+ customStyle={{
286
+ margin: 0,
287
+ padding: '1rem',
288
+ background: 'rgba(127, 29, 29, 0.1)',
289
+ fontSize: '0.75rem',
290
+ maxHeight: '300px',
291
+ }}
292
+ lineNumberStyle={{
293
+ minWidth: '2.5em',
294
+ paddingRight: '1em',
295
+ color: '#ef4444',
296
+ opacity: 0.5
297
+ }}
298
+ >
299
+ {tool.args.oldString || ''}
300
+ </SyntaxHighlighter>
301
+ </div>
302
+ </div>
303
+
304
+ <div className="space-y-1">
305
+ <div className="text-[10px] font-black uppercase text-emerald-400 tracking-wider flex items-center gap-2">
306
+ <span>Added</span>
307
+ <span className="font-mono text-[9px]">{tool.args.newString?.split('\n').length || 0} lines</span>
308
+ </div>
309
+ <div className="rounded-lg overflow-hidden border border-emerald-900/30 bg-emerald-950/10">
310
+ <SyntaxHighlighter
311
+ language={getLanguageFromFilePath(tool.args.filePath)}
312
+ style={vscDarkPlus}
313
+ showLineNumbers={true}
314
+ customStyle={{
315
+ margin: 0,
316
+ padding: '1rem',
317
+ background: 'rgba(6, 78, 59, 0.1)',
318
+ fontSize: '0.75rem',
319
+ maxHeight: '300px',
320
+ }}
321
+ lineNumberStyle={{
322
+ minWidth: '2.5em',
323
+ paddingRight: '1em',
324
+ color: '#10b981',
325
+ opacity: 0.5
326
+ }}
327
+ >
328
+ {tool.args.newString || ''}
329
+ </SyntaxHighlighter>
330
+ </div>
331
+ </div>
332
+ </>
333
+ ) : (
334
+ // Standard Tool Display
335
+ <>
336
+ <div className="space-y-1">
337
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">
338
+ {tool.name === 'bash' ? 'Command' : 'Input'}
339
+ </div>
340
+ <div className={cn(
341
+ "font-mono text-xs p-3 rounded-lg border overflow-x-auto whitespace-pre-wrap break-words",
342
+ tool.name === 'bash'
343
+ ? "bg-zinc-950 border-zinc-800 text-emerald-400"
344
+ : "bg-zinc-950/50 border-zinc-900 text-zinc-400"
345
+ )}>
346
+ {formatToolArgs(tool)}
347
+ </div>
348
+ </div>
349
+
350
+ {/* Output Result */}
351
+ {tool.result && (
352
+ <div className="space-y-1">
353
+ <div className="text-[10px] font-black uppercase text-zinc-600 tracking-wider">Output</div>
354
+ <div className={cn(
355
+ "font-mono text-xs p-3 rounded-lg border overflow-x-auto whitespace-pre-wrap break-words max-h-96 overflow-y-auto",
356
+ isError
357
+ ? "bg-zinc-950 border-red-900/30 text-red-400"
358
+ : tool.name === 'bash'
359
+ ? "bg-zinc-950 border-zinc-800 text-zinc-300"
360
+ : "bg-zinc-950/50 border-zinc-900 text-emerald-500/80"
361
+ )}>
362
+ {formatToolResult(tool)}
363
+ </div>
364
+ </div>
365
+ )}
366
+ </>
367
+ )}
368
+ </div>
369
+ )}
370
+ </div>
371
+ );
372
+ }
373
+
374
+ export function MessageItem({ message }: { message: Message }) {
375
+ const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
376
+ const isUser = message.role === 'user';
377
+
378
+ // Format timestamp
379
+ const formatTime = (timestamp: string | Date) => {
380
+ const date = new Date(timestamp);
381
+ const now = new Date();
382
+ const diff = now.getTime() - date.getTime();
383
+ const seconds = Math.floor(diff / 1000);
384
+ const minutes = Math.floor(seconds / 60);
385
+ const hours = Math.floor(minutes / 60);
386
+ const days = Math.floor(hours / 24);
387
+
388
+ if (days > 0) return `${days}d ago`;
389
+ if (hours > 0) return `${hours}h ago`;
390
+ if (minutes > 0) return `${minutes}m ago`;
391
+ if (seconds > 30) return `${seconds}s ago`;
392
+ return 'just now';
393
+ };
394
+
395
+ return (
396
+ <div className={cn("w-full py-6 border-b border-zinc-900/30 transition-colors", !isUser ? "bg-zinc-900/5" : "bg-transparent")}>
397
+ <div className="max-w-4xl mx-auto flex gap-6 px-6 md:px-12">
398
+ <div className={cn("w-8 h-8 rounded shrink-0 flex items-center justify-center shadow-lg transition-transform", isUser ? "bg-zinc-800 text-zinc-300" : "bg-cyan-900/20 text-cyan-500")}>
399
+ {isUser ? <User size={16} /> : <Bot size={16} />}
400
+ </div>
401
+
402
+ <div className="flex-1 min-w-0 space-y-4">
403
+ {message.thinking && (
404
+ <div className="mb-4">
405
+ <button
406
+ type="button"
407
+ onClick={() => setIsThinkingExpanded(!isThinkingExpanded)}
408
+ className="flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors select-none group"
409
+ >
410
+ <span>{isThinkingExpanded ? 'Hide thought' : 'Show thought'}</span>
411
+ <span className="opacity-50">·</span>
412
+ <span className="font-mono text-[10px] opacity-60">{formatTime(message.timestamp)}</span>
413
+ <div className="flex items-center justify-center w-4 h-4 opacity-70 group-hover:opacity-100 transition-opacity ml-0.5">
414
+ <svg
415
+ fill="none"
416
+ viewBox="0 0 20 20"
417
+ aria-hidden="true"
418
+ className={cn("w-4 h-4 transition-transform duration-200", isThinkingExpanded && "rotate-180")}
419
+ >
420
+ <path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" strokeLinecap="square"></path>
421
+ </svg>
422
+ </div>
423
+ </button>
424
+
425
+ {isThinkingExpanded && (
426
+ <div className="mt-2 pl-3 border-l-2 border-amber-500/20 ml-1 text-sm text-zinc-500 font-mono leading-relaxed animate-in fade-in slide-in-from-top-1 duration-200">
427
+ {message.thinking}
428
+ </div>
429
+ )}
430
+ </div>
431
+ )}
432
+
433
+ {message.toolCalls && message.toolCalls.length > 0 && (
434
+ <ToolSteps tools={message.toolCalls} />
435
+ )}
436
+
437
+ <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">
438
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
439
+ {message.content}
440
+ </ReactMarkdown>
441
+ </div>
442
+ </div>
443
+ </div>
444
+ </div>
445
+ );
446
+ }
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { Terminal as LucideTerminal, Clock, Trash2, Zap, MoreHorizontal, Activity } from 'lucide-react';
3
+
4
+ interface CommandLogEntry {
5
+ id: string;
6
+ command: string;
7
+ timestamp: string;
8
+ status: 'running' | 'completed' | 'failed';
9
+ output?: string;
10
+ }
11
+
12
+ interface CommandInspectorProps {
13
+ entries: CommandLogEntry[];
14
+ isVisible: boolean;
15
+ onClose: () => void;
16
+ }
17
+
18
+ export function CommandInspector({ entries, isVisible, onClose }: CommandInspectorProps) {
19
+ if (!isVisible) return null;
20
+
21
+ return (
22
+ <div className="absolute inset-0 bg-zinc-950 z-50 flex flex-col overflow-hidden animate-in fade-in duration-300">
23
+ <div className="h-16 bg-zinc-900 border-b border-zinc-800 flex items-center justify-between px-8 shrink-0">
24
+ <div className="flex items-center gap-4">
25
+ <div className="w-10 h-10 rounded-xl bg-amber-600/10 flex items-center justify-center text-amber-500 shadow-inner border border-amber-500/20">
26
+ <LucideTerminal size={20} />
27
+ </div>
28
+ <div>
29
+ <p className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">Terminal Command Logs</p>
30
+ <h3 className="text-sm font-bold text-white italic">Execution Pipeline</h3>
31
+ </div>
32
+ </div>
33
+ <button onClick={onClose} className="p-2 hover:bg-zinc-800 rounded-xl text-zinc-500 hover:text-white transition-all">
34
+ <MoreHorizontal size={20} />
35
+ </button>
36
+ </div>
37
+
38
+ <div className="flex-1 overflow-y-auto custom-scrollbar bg-black/50 p-6 md:p-10 space-y-4">
39
+ {entries.map(entry => (
40
+ <div key={entry.id} className="bg-zinc-900/30 border border-zinc-900 rounded-2xl p-6 space-y-4 group hover:border-zinc-800 transition-all">
41
+ <div className="flex items-center justify-between">
42
+ <div className="flex items-center gap-4">
43
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
44
+ entry.status === 'completed' ? 'bg-emerald-600/10 text-emerald-500' :
45
+ entry.status === 'failed' ? 'bg-rose-600/10 text-rose-500' : 'bg-amber-600/10 text-amber-500'
46
+ }`}>
47
+ <Activity size={14} className={entry.status === 'running' ? 'animate-spin' : ''} />
48
+ </div>
49
+ <code className="text-xs font-bold text-zinc-200 uppercase tracking-widest">{entry.command}</code>
50
+ </div>
51
+ <div className="flex items-center gap-3">
52
+ <span className="text-[9px] font-black text-zinc-700 uppercase tracking-widest">{new Date(entry.timestamp).toLocaleTimeString()}</span>
53
+ </div>
54
+ </div>
55
+ {entry.output && (
56
+ <div className="bg-zinc-950 p-6 rounded-xl border border-zinc-900 font-mono text-[11px] text-zinc-500 overflow-x-auto whitespace-pre-wrap shadow-inner leading-relaxed opacity-60 group-hover:opacity-100 transition-opacity">
57
+ {entry.output}
58
+ </div>
59
+ )}
60
+ </div>
61
+ ))}
62
+ {entries.length === 0 && (
63
+ <div className="h-full flex flex-col items-center justify-center opacity-10 space-y-4 py-20 uppercase tracking-[0.5em] text-zinc-500">
64
+ <LucideTerminal size={64} />
65
+ <p className="text-[10px] font-black">No system commands logged</p>
66
+ </div>
67
+ )}
68
+ </div>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ import { FileCode, Trash2, Check, ArrowRight, Zap, Code } from 'lucide-react';
5
+
6
+ interface DiffViewerProps {
7
+ filename: string;
8
+ before: string;
9
+ after: string;
10
+ isVisible: boolean;
11
+ onClose: () => void;
12
+ }
13
+
14
+ export function DiffViewer({ filename, before, after, isVisible, onClose }: DiffViewerProps) {
15
+ if (!isVisible) return null;
16
+
17
+ return (
18
+ <div className="absolute inset-0 bg-zinc-950 z-50 flex flex-col overflow-hidden animate-in fade-in duration-300">
19
+ <div className="h-16 bg-zinc-900 border-b border-zinc-800 flex items-center justify-between px-8 shrink-0">
20
+ <div className="flex items-center gap-4">
21
+ <div className="w-10 h-10 rounded-xl bg-cyan-600/10 flex items-center justify-center text-cyan-500 shadow-inner">
22
+ <FileCode size={20} />
23
+ </div>
24
+ <div>
25
+ <p className="text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">Live Stage Edit</p>
26
+ <h3 className="text-sm font-bold text-white font-mono italic">{filename}</h3>
27
+ </div>
28
+ </div>
29
+ <div className="flex items-center gap-4">
30
+ <div className="flex items-center gap-2 px-4 py-1.5 bg-zinc-950 border border-zinc-800 rounded-lg text-[9px] font-black uppercase tracking-widest text-zinc-600">
31
+ <span className="text-emerald-500">+ {after.split('\n').length} lines</span>
32
+ <span className="opacity-20">/</span>
33
+ <span className="text-rose-500">- {before.split('\n').length} lines</span>
34
+ </div>
35
+ <button onClick={onClose} className="p-2 hover:bg-zinc-800 rounded-xl text-zinc-500 hover:text-white transition-all">
36
+ <Check size={20} />
37
+ </button>
38
+ </div>
39
+ </div>
40
+
41
+ <div className="flex-1 flex overflow-hidden">
42
+ <div className="flex-1 border-r border-zinc-900 overflow-hidden flex flex-col relative">
43
+ <div className="absolute top-4 right-6 z-10 px-3 py-1 bg-zinc-950/80 backdrop-blur rounded text-[9px] font-black uppercase text-zinc-700 tracking-widest border border-white/5">Original Segment</div>
44
+ <div className="flex-1 overflow-auto custom-scrollbar p-6 bg-zinc-950/50">
45
+ <SyntaxHighlighter
46
+ language="typescript"
47
+ style={vscDarkPlus}
48
+ customStyle={{ background: 'transparent', fontSize: '0.8rem', margin: 0 }}
49
+ >
50
+ {before}
51
+ </SyntaxHighlighter>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="flex-1 overflow-hidden flex flex-col relative bg-zinc-900/10">
56
+ <div className="absolute top-4 right-6 z-10 px-3 py-1 bg-cyan-900/20 backdrop-blur rounded text-[9px] font-black uppercase text-cyan-500 tracking-widest border border-cyan-500/20 shadow-[0_0_15px_rgba(6,182,212,0.1)]">Target Output</div>
57
+ <div className="flex-1 overflow-auto custom-scrollbar p-6 bg-zinc-950/30">
58
+ <SyntaxHighlighter
59
+ language="typescript"
60
+ style={vscDarkPlus}
61
+ customStyle={{ background: 'transparent', fontSize: '0.8rem', margin: 0 }}
62
+ >
63
+ {after}
64
+ </SyntaxHighlighter>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div className="h-12 bg-zinc-900 border-t border-zinc-800 px-8 flex items-center justify-between shrink-0">
70
+ <div className="flex items-center gap-6">
71
+ <div className="flex items-center gap-2">
72
+ <div className="w-1.5 h-1.5 rounded-full bg-cyan-500 animate-pulse" />
73
+ <span className="text-[9px] font-black uppercase text-zinc-700 tracking-widest italic">Stream writing payload...</span>
74
+ </div>
75
+ </div>
76
+ <div className="flex items-center gap-2 text-[9px] font-black uppercase text-zinc-700">
77
+ <Zap size={10} className="text-amber-600" />
78
+ <span>Real-time integrity check: OK</span>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ );
83
+ }