@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.
- package/README.md +5 -0
- package/bin/{jarvis.js → jarvis} +1 -1
- package/dist/cli.js +476 -350
- package/dist/electron/main.js +160 -0
- package/dist/electron/preload.js +19 -0
- package/package.json +21 -8
- package/skills.md +147 -0
- package/src/agents/index.ts +248 -0
- package/src/brain/loader.ts +136 -0
- package/src/cli.ts +411 -0
- package/src/config/index.ts +363 -0
- package/src/core/executor.ts +222 -0
- package/src/core/plugins.ts +148 -0
- package/src/core/types.ts +217 -0
- package/src/electron/main.ts +192 -0
- package/src/electron/preload.ts +25 -0
- package/src/electron/types.d.ts +20 -0
- package/src/index.ts +12 -0
- package/src/providers/antigravity-loader.ts +233 -0
- package/src/providers/antigravity.ts +585 -0
- package/src/providers/index.ts +523 -0
- package/src/sessions/index.ts +194 -0
- package/src/tools/index.ts +436 -0
- package/src/tui/index.tsx +784 -0
- package/src/utils/auth-prompt.ts +394 -0
- package/src/utils/index.ts +180 -0
- package/src/utils/native-picker.ts +71 -0
- package/src/utils/skills.ts +99 -0
- package/src/utils/table-integration-examples.ts +617 -0
- package/src/utils/table-utils.ts +401 -0
- package/src/web/build-ui.ts +27 -0
- package/src/web/server.ts +674 -0
- package/src/web/ui/dist/.gitkeep +0 -0
- package/src/web/ui/dist/main.css +1 -0
- package/src/web/ui/dist/main.js +320 -0
- package/src/web/ui/dist/main.js.map +20 -0
- package/src/web/ui/index.html +46 -0
- package/src/web/ui/src/App.tsx +143 -0
- package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
- package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
- package/src/web/ui/src/components/Layout/Header.tsx +91 -0
- package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
- package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
- package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
- package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
- package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
- package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
- package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
- package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
- package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
- package/src/web/ui/src/config/models.ts +70 -0
- package/src/web/ui/src/main.tsx +13 -0
- package/src/web/ui/src/store/agentStore.ts +41 -0
- package/src/web/ui/src/store/uiStore.ts +64 -0
- 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
|
+
}
|