@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,895 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Plus, MessageSquare, Folder, Hash, Settings, Bot, ChevronRight,
4
+ Trash2, MoreHorizontal, Clock, Search, ChevronLeft, Layout,
5
+ FolderPlus, HelpCircle, ChevronDown, X, Loader2
6
+ } from 'lucide-react';
7
+ import { motion, AnimatePresence } from 'framer-motion';
8
+ import { useUIStore, type Workspace } from '../../store/uiStore';
9
+ import type { Session, SidebarProps } from '../../types';
10
+
11
+ const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
12
+
13
+ function SessionNode({
14
+ session,
15
+ allSessions,
16
+ depth = 0,
17
+ currentSessionId,
18
+ onSessionSelect,
19
+ onContextMenu
20
+ }: {
21
+ session: Session,
22
+ allSessions: Session[],
23
+ depth?: number,
24
+ currentSessionId: string | null,
25
+ onSessionSelect: (id: string) => void,
26
+ onContextMenu?: (e: React.MouseEvent, session: Session) => void
27
+ }) {
28
+ const [isOpen, setIsOpen] = useState(true);
29
+ const children = allSessions.filter(s => s.parentId === session.id);
30
+ const hasChildren = children.length > 0;
31
+ const isActive = currentSessionId === session.id;
32
+
33
+ return (
34
+ <div className="space-y-1">
35
+ <button
36
+ onClick={() => onSessionSelect(session.id)}
37
+ onContextMenu={(e) => onContextMenu?.(e, session)}
38
+ className={cn(
39
+ "flex items-center gap-2 w-full p-2 rounded-lg text-left transition-all group relative",
40
+ isActive
41
+ ? "bg-cyan-900/20 text-cyan-200 shadow-[0_0_10px_rgba(6,182,212,0.1)] ring-1 ring-cyan-500/30"
42
+ : "text-zinc-500 hover:bg-zinc-900/50 hover:text-zinc-300"
43
+ )}
44
+ style={{ paddingLeft: `${(depth * 12) + 8}px` }}
45
+ >
46
+ {hasChildren && (
47
+ <div
48
+ onClick={(e) => { e.stopPropagation(); setIsOpen(!isOpen); }}
49
+ className={cn(
50
+ "p-0.5 rounded transition-colors shrink-0",
51
+ isActive ? "text-cyan-500 hover:bg-cyan-900/30" : "hover:bg-zinc-700"
52
+ )}
53
+ >
54
+ {isOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
55
+ </div>
56
+ )}
57
+ {!hasChildren && depth > 0 && <div className="w-3 shrink-0" />}
58
+ <div className={cn("w-1.5 h-1.5 rounded-full shrink-0 shadow-[0_0_5px_currentColor]", isActive ? "bg-cyan-500" : "bg-zinc-700")} />
59
+ <span className="text-xs font-bold truncate flex-1">{session.title || "New Protocol"}</span>
60
+ </button>
61
+
62
+ {hasChildren && isOpen && (
63
+ <div className="space-y-1">
64
+ {children.map(child => (
65
+ <SessionNode
66
+ key={child.id}
67
+ session={child}
68
+ allSessions={allSessions}
69
+ depth={depth + 1}
70
+ currentSessionId={currentSessionId}
71
+ onSessionSelect={onSessionSelect}
72
+ onContextMenu={onContextMenu}
73
+ />
74
+ ))}
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
80
+
81
+ export function Sidebar({ isCollapsed, onToggle, currentSessionId, onSessionSelect }: Omit<SidebarProps, 'activeModule' | 'onModuleChange'>) {
82
+ const {
83
+ workspaces, setWorkspaces, addWorkspace, removeWorkspace,
84
+ activeWorkspaceId, setActiveWorkspace,
85
+ activeModule, setActiveModule,
86
+ setSettingsOpen
87
+ } = useUIStore();
88
+
89
+ const [sessions, setSessions] = useState<Session[]>([]);
90
+ const [searchQuery, setSearchQuery] = useState('');
91
+ const [isAddingWorkspace, setIsAddingWorkspace] = useState(false);
92
+ const [showFolderModal, setShowFolderModal] = useState(false);
93
+ const [selectedFolderPath, setSelectedFolderPath] = useState<string>('');
94
+ const [selectedFolderName, setSelectedFolderName] = useState<string>('');
95
+ const [deleteConfirmModal, setDeleteConfirmModal] = useState<{
96
+ type: 'workspace' | 'session';
97
+ id: string;
98
+ name: string;
99
+ } | null>(null);
100
+ const [showCleanConfirm, setShowCleanConfirm] = useState(false);
101
+ const [contextMenu, setContextMenu] = useState<{
102
+ x: number;
103
+ y: number;
104
+ type: 'workspace' | 'session';
105
+ id: string;
106
+ name: string;
107
+ } | null>(null);
108
+
109
+ const folderInputRef = React.useRef<HTMLInputElement>(null);
110
+
111
+ // Fetch workspaces on load
112
+ useEffect(() => {
113
+ const loadWorkspaces = async () => {
114
+ try {
115
+ console.log('[Sidebar] Loading workspaces...');
116
+ const res = await fetch('/api/workspaces');
117
+ const data = await res.json();
118
+
119
+ if (data.workspaces && Array.isArray(data.workspaces)) {
120
+ console.log('[Sidebar] Loaded workspaces:', data.workspaces.length);
121
+
122
+ // Set all workspaces at once
123
+ setWorkspaces(data.workspaces);
124
+
125
+ // Auto-activate first workspace if none active
126
+ if (!activeWorkspaceId && data.workspaces.length > 0) {
127
+ setActiveWorkspace(data.workspaces[0].id);
128
+ }
129
+ }
130
+ } catch (e) {
131
+ console.error('[Sidebar] Failed to load workspaces:', e);
132
+ }
133
+ };
134
+
135
+ loadWorkspaces();
136
+ }, []);
137
+
138
+ const activeWorkspace = workspaces.find(w => w.id === activeWorkspaceId);
139
+
140
+ const refreshSessions = async () => {
141
+ try {
142
+ const url = activeWorkspace ? `/api/sessions?workdir=${encodeURIComponent(activeWorkspace.path)}` : '/api/sessions';
143
+ const res = await fetch(url);
144
+ const data = await res.json();
145
+ if (Array.isArray(data)) setSessions(data);
146
+ } catch (e) { console.error(e); }
147
+ };
148
+
149
+ useEffect(() => {
150
+ refreshSessions();
151
+ }, [activeWorkspaceId]);
152
+
153
+ const handleFolderSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
154
+ const files = e.target.files;
155
+
156
+ if (!files || files.length === 0) {
157
+ console.log('[Sidebar] No folder selected');
158
+ return;
159
+ }
160
+
161
+ // Extract folder name from webkitRelativePath
162
+ // Example: "MyProject/src/index.js" -> "MyProject"
163
+ const firstFile = files[0];
164
+ if (!firstFile) return;
165
+
166
+ const pathParts = firstFile.webkitRelativePath.split('/');
167
+ const folderName = pathParts[0] || 'Workspace';
168
+
169
+ console.log('[Sidebar] Folder selected:', folderName);
170
+
171
+ // Set folder info for modal confirmation
172
+ setSelectedFolderPath(folderName);
173
+ setSelectedFolderName(folderName);
174
+ setShowFolderModal(true);
175
+ };
176
+
177
+ const handleAddWorkspace = async () => {
178
+ if (isAddingWorkspace) return;
179
+
180
+ try {
181
+ // Check if running in Electron
182
+ if (typeof window !== 'undefined' && window.electron?.isElectron) {
183
+ console.log('[Sidebar] Using Electron native folder picker');
184
+
185
+ // Use Electron's native folder picker
186
+ const folderPath = await window.electron.selectFolder();
187
+
188
+ if (!folderPath) {
189
+ console.log('[Sidebar] User cancelled folder selection');
190
+ return;
191
+ }
192
+
193
+ // Extract folder name from full path
194
+ const folderName = folderPath.split(/[\\/]/).pop() || 'Workspace';
195
+
196
+ console.log('[Sidebar] Selected folder:', folderName, 'Path:', folderPath);
197
+
198
+ // Show modal with selected folder
199
+ setSelectedFolderPath(folderPath);
200
+ setSelectedFolderName(folderName);
201
+ setShowFolderModal(true);
202
+
203
+ } else {
204
+ // Use HTML5 directory picker for web mode
205
+ console.log('[Sidebar] Using HTML5 directory picker');
206
+ folderInputRef.current?.click();
207
+ }
208
+
209
+ } catch (e) {
210
+ console.error('[Sidebar] Failed to select folder:', e);
211
+ }
212
+ };
213
+
214
+ const confirmAddWorkspace = async () => {
215
+ if (!selectedFolderPath || !selectedFolderName) return;
216
+
217
+ setIsAddingWorkspace(true);
218
+
219
+ try {
220
+ // Create workspace with the selected path
221
+ const res = await fetch('/api/workspaces', {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify({
225
+ name: selectedFolderName,
226
+ path: selectedFolderPath
227
+ })
228
+ });
229
+
230
+ const data = await res.json();
231
+
232
+ if (!res.ok) {
233
+ if (res.status === 409 && data.workspace) {
234
+ addWorkspace(data.workspace);
235
+ setActiveWorkspace(data.workspace.id);
236
+ } else {
237
+ console.error(`Failed to create workspace: ${data.error}`);
238
+ }
239
+ } else if (data.id) {
240
+ console.log('[Sidebar] Workspace created:', data);
241
+ addWorkspace(data);
242
+ setActiveWorkspace(data.id);
243
+ refreshSessions();
244
+ }
245
+
246
+ // Close modal and reset
247
+ setShowFolderModal(false);
248
+ setSelectedFolderPath('');
249
+ setSelectedFolderName('');
250
+
251
+ // Reset input to allow selecting same folder again
252
+ if (folderInputRef.current) {
253
+ folderInputRef.current.value = '';
254
+ }
255
+ } catch (e) {
256
+ console.error('[Sidebar] Failed to create workspace:', e);
257
+ } finally {
258
+ setIsAddingWorkspace(false);
259
+ }
260
+ };
261
+
262
+ const handleDeleteWorkspace = async (e: React.MouseEvent, id: string) => {
263
+ e.stopPropagation();
264
+
265
+ // Find workspace name
266
+ const workspace = workspaces.find(w => w.id === id);
267
+ if (!workspace) return;
268
+
269
+ // Show delete confirmation modal
270
+ setDeleteConfirmModal({
271
+ type: 'workspace',
272
+ id: id,
273
+ name: workspace.name
274
+ });
275
+ };
276
+
277
+ // Context menu handlers
278
+ const handleWorkspaceContextMenu = (e: React.MouseEvent, ws: Workspace) => {
279
+ e.preventDefault();
280
+ e.stopPropagation();
281
+ console.log('[Sidebar] Workspace context menu:', ws.name);
282
+
283
+ setContextMenu({
284
+ x: e.clientX,
285
+ y: e.clientY,
286
+ type: 'workspace',
287
+ id: ws.id,
288
+ name: ws.name
289
+ });
290
+ };
291
+
292
+ const handleSessionContextMenu = (e: React.MouseEvent, session: Session) => {
293
+ e.preventDefault();
294
+ e.stopPropagation();
295
+ console.log('[Sidebar] Session context menu:', session.title);
296
+
297
+ setContextMenu({
298
+ x: e.clientX,
299
+ y: e.clientY,
300
+ type: 'session',
301
+ id: session.id,
302
+ name: session.title
303
+ });
304
+ };
305
+
306
+ const handleContextMenuDelete = () => {
307
+ if (!contextMenu) return;
308
+
309
+ // Show delete confirmation modal
310
+ setDeleteConfirmModal({
311
+ type: contextMenu.type,
312
+ id: contextMenu.id,
313
+ name: contextMenu.name
314
+ });
315
+
316
+ setContextMenu(null);
317
+ };
318
+
319
+ const confirmDelete = async () => {
320
+ if (!deleteConfirmModal) return;
321
+
322
+ try {
323
+ if (deleteConfirmModal.type === 'workspace') {
324
+ const res = await fetch(`/api/workspaces/${deleteConfirmModal.id}`, { method: 'DELETE' });
325
+ if (res.ok) {
326
+ console.log('[Sidebar] Workspace deleted:', deleteConfirmModal.id);
327
+ removeWorkspace(deleteConfirmModal.id);
328
+ } else {
329
+ throw new Error('Delete request failed');
330
+ }
331
+ } else {
332
+ await fetch(`/api/sessions/${deleteConfirmModal.id}`, { method: 'DELETE' });
333
+ refreshSessions();
334
+ }
335
+ } catch (e) {
336
+ console.error(`Failed to delete ${deleteConfirmModal.type}:`, e);
337
+ }
338
+
339
+ setDeleteConfirmModal(null);
340
+ };
341
+
342
+ const confirmCleanWorkspaces = async () => {
343
+ try {
344
+ const res = await fetch('/api/workspaces', { method: 'DELETE' });
345
+ if (res.ok) {
346
+ console.log('[Sidebar] All workspaces cleaned');
347
+ setWorkspaces([]);
348
+ setActiveWorkspace(null);
349
+ }
350
+ } catch (e) {
351
+ console.error('[Sidebar] Failed to clean workspaces:', e);
352
+ }
353
+
354
+ setShowCleanConfirm(false);
355
+ };
356
+
357
+ // Close context menu on outside click
358
+ useEffect(() => {
359
+ const handleClickOutside = () => setContextMenu(null);
360
+ if (contextMenu) {
361
+ document.addEventListener('click', handleClickOutside);
362
+ return () => document.removeEventListener('click', handleClickOutside);
363
+ }
364
+ }, [contextMenu]);
365
+
366
+ const handleModuleClick = (module: 'chat' | 'settings') => {
367
+ setActiveModule(module);
368
+ };
369
+
370
+ const createNewSession = async () => {
371
+ if (!activeWorkspace) {
372
+ console.warn('[Sidebar] No workspace selected');
373
+ return;
374
+ }
375
+
376
+ try {
377
+ console.log('[Sidebar] Creating session in workspace:', activeWorkspace.path);
378
+
379
+ const res = await fetch('/api/sessions', {
380
+ method: 'POST',
381
+ headers: { 'Content-Type': 'application/json' },
382
+ body: JSON.stringify({ agentId: 'build', workdir: activeWorkspace.path })
383
+ });
384
+
385
+ const data = await res.json();
386
+ console.log('[Sidebar] Session created:', data.id);
387
+
388
+ onSessionSelect(data.id);
389
+ handleModuleClick('chat');
390
+ refreshSessions();
391
+ } catch (e) {
392
+ console.error('[Sidebar] Failed to create session:', e);
393
+ }
394
+ };
395
+
396
+ // Only top-level sessions for the root list
397
+ const rootSessions = sessions.filter(s => !s.parentId);
398
+
399
+ return (
400
+ <div className="flex h-full bg-zinc-950 border-r border-zinc-900 overflow-hidden shrink-0 z-40 shadow-2xl">
401
+
402
+ {/* FAR LEFT: WORKSPACE RAIL */}
403
+ <div className="w-16 flex flex-col items-center py-4 gap-4 border-r border-zinc-900/50 bg-zinc-950 shrink-0">
404
+ <div
405
+ onClick={() => handleModuleClick('chat')}
406
+ className="w-10 h-10 rounded-xl bg-cyan-600 flex items-center justify-center text-white font-black shadow-lg shadow-cyan-900/20 cursor-pointer hover:scale-105 transition-transform"
407
+ >
408
+ J
409
+ </div>
410
+
411
+ <div className="w-8 h-px bg-zinc-800" />
412
+
413
+ <div className="flex flex-col gap-3 flex-1 overflow-y-auto no-scrollbar py-2">
414
+ {/* Workspace Icons */}
415
+ {workspaces.map(ws => (
416
+ <div
417
+ key={ws.id}
418
+ title={`${ws.name}\n${ws.path}`}
419
+ onClick={() => {
420
+ console.log('[Sidebar] Switching to workspace:', ws.name);
421
+ setActiveWorkspace(ws.id);
422
+ handleModuleClick('chat');
423
+ }}
424
+ onContextMenu={(e) => handleWorkspaceContextMenu(e, ws)}
425
+ className={cn(
426
+ "w-10 h-10 rounded-xl flex items-center justify-center text-xs font-black cursor-pointer transition-all border shrink-0 relative group/ws",
427
+ activeWorkspaceId === ws.id
428
+ ? "bg-cyan-900/20 border-cyan-500 text-cyan-400 shadow-[0_0_15px_rgba(6,182,212,0.3)] ring-2 ring-cyan-500/20"
429
+ : "bg-zinc-900 border-zinc-800 text-zinc-600 hover:border-zinc-700 hover:text-zinc-400 hover:bg-zinc-800/50"
430
+ )}
431
+ >
432
+ {/* Pulse animation for active */}
433
+ {activeWorkspaceId === ws.id && (
434
+ <div className="absolute inset-0 bg-cyan-400/10 rounded-xl animate-pulse" />
435
+ )}
436
+
437
+ {/* Icon */}
438
+ <span className="relative z-10">{ws.icon}</span>
439
+
440
+ {/* Delete button */}
441
+ <button
442
+ onClick={(e) => handleDeleteWorkspace(e, ws.id)}
443
+ className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-red-600 text-white flex items-center justify-center opacity-0 group-hover/ws:opacity-100 hover:bg-red-500 transition-all z-20 shadow-lg"
444
+ title="Delete workspace"
445
+ >
446
+ <X size={8} strokeWidth={4} />
447
+ </button>
448
+ </div>
449
+ ))}
450
+
451
+ <button
452
+ onClick={handleAddWorkspace}
453
+ disabled={isAddingWorkspace}
454
+ className={cn(
455
+ "w-10 h-10 rounded-xl flex items-center justify-center border border-dashed transition-all cursor-pointer group relative",
456
+ isAddingWorkspace
457
+ ? "bg-zinc-900 border-cyan-500 text-cyan-500 animate-pulse cursor-wait"
458
+ : "bg-zinc-950 border-zinc-900 text-zinc-700 hover:border-zinc-600 hover:text-zinc-400"
459
+ )}
460
+ title="Add workspace (browse folder)"
461
+ >
462
+ {isAddingWorkspace ? (
463
+ <div className="animate-spin">
464
+ <Loader2 size={18} />
465
+ </div>
466
+ ) : (
467
+ <Plus size={18} className="group-hover:scale-110 transition-transform" />
468
+ )}
469
+ </button>
470
+
471
+ {/* Hidden directory input for HTML5 folder picker */}
472
+ <input
473
+ type="file"
474
+ ref={folderInputRef}
475
+ className="hidden"
476
+ onChange={handleFolderSelect}
477
+ {...({ webkitdirectory: "", directory: "" } as any)}
478
+ />
479
+ </div>
480
+
481
+ <div className="flex flex-col gap-4 mt-auto">
482
+ {workspaces.length > 0 && (
483
+ <button
484
+ onClick={() => setShowCleanConfirm(true)}
485
+ className="text-zinc-700 hover:text-red-400 transition-colors"
486
+ title="Clean all workspaces"
487
+ >
488
+ <Trash2 size={20} />
489
+ </button>
490
+ )}
491
+ <button
492
+ onClick={() => setSettingsOpen(true)}
493
+ className="text-zinc-700 hover:text-zinc-400 transition-colors"
494
+ >
495
+ <Settings size={20} />
496
+ </button>
497
+ <button className="text-zinc-700 hover:text-zinc-400 transition-colors">
498
+ <HelpCircle size={20} />
499
+ </button>
500
+ </div>
501
+ </div>
502
+
503
+ {/* SESSION EXPLORER (Collapsible) */}
504
+ <motion.div
505
+ initial={false}
506
+ animate={{ width: isCollapsed ? 0 : 260 }}
507
+ className="flex flex-col bg-zinc-950 h-full overflow-hidden"
508
+ >
509
+ <div className="w-[260px] p-4 flex flex-col h-full">
510
+ {/* Header */}
511
+ <div className="mb-6 px-1 flex flex-col gap-1">
512
+ <div className="flex items-center justify-between">
513
+ <h2 className="text-lg font-black text-zinc-100 tracking-tighter italic uppercase">jarvis</h2>
514
+ <button onClick={onToggle} className="text-zinc-700 hover:text-zinc-400 transition-colors">
515
+ <ChevronLeft size={16} />
516
+ </button>
517
+ </div>
518
+ <p className="text-[10px] text-zinc-600 font-mono truncate max-w-full opacity-60 leading-tight">
519
+ {activeWorkspace?.path || "D:\\Benz Siangco Creatives\\VibeCode\\opencode\\jar..."}
520
+ </p>
521
+ </div>
522
+
523
+ <button
524
+ onClick={createNewSession}
525
+ className="flex items-center justify-center gap-2 w-full p-2.5 bg-zinc-900/50 border border-zinc-800/50 rounded-xl hover:bg-zinc-800 transition-all mb-8 group active:scale-[0.98]"
526
+ >
527
+ <Plus size={16} className="text-zinc-500 group-hover:text-white" />
528
+ <span className="text-xs font-bold text-zinc-300">New session</span>
529
+ </button>
530
+
531
+ {/* Sessions Tree - Grouped by Workspace */}
532
+ <div className="flex-1 overflow-y-auto no-scrollbar -mx-2 px-2">
533
+ {activeWorkspace ? (
534
+ <>
535
+ {/* Active workspace header */}
536
+ <div className="px-2 py-2 mb-2">
537
+ <p className="text-[10px] text-zinc-500 uppercase tracking-wider font-bold">
538
+ {activeWorkspace.name}
539
+ </p>
540
+ </div>
541
+
542
+ {/* Sessions for active workspace */}
543
+ <div className="space-y-1">
544
+ {rootSessions
545
+ .filter(s => !s.workdir || s.workdir === activeWorkspace.path)
546
+ .map((s) => (
547
+ <SessionNode
548
+ key={s.id}
549
+ session={s}
550
+ allSessions={sessions}
551
+ currentSessionId={currentSessionId}
552
+ onSessionSelect={onSessionSelect}
553
+ onContextMenu={handleSessionContextMenu}
554
+ />
555
+ ))}
556
+ </div>
557
+
558
+ {/* Show other workspaces sessions (collapsed) */}
559
+ {workspaces.filter(ws => ws.id !== activeWorkspaceId).map(ws => {
560
+ const wsSessions = rootSessions.filter(s => s.workdir === ws.path);
561
+ if (wsSessions.length === 0) return null;
562
+
563
+ return (
564
+ <details key={ws.id} className="mt-4">
565
+ <summary className="px-2 py-2 cursor-pointer hover:bg-zinc-900/30 rounded-lg text-[10px] text-zinc-600 uppercase tracking-wider font-bold flex items-center gap-2 list-none">
566
+ <ChevronRight size={12} className="transition-transform [[details[open]>summary>&]]:rotate-90" />
567
+ {ws.name} ({wsSessions.length})
568
+ </summary>
569
+ <div className="mt-2 space-y-1 pl-2">
570
+ {wsSessions.map(session => (
571
+ <SessionNode
572
+ key={session.id}
573
+ session={session}
574
+ allSessions={sessions}
575
+ currentSessionId={currentSessionId}
576
+ onSessionSelect={onSessionSelect}
577
+ onContextMenu={handleSessionContextMenu}
578
+ />
579
+ ))}
580
+ </div>
581
+ </details>
582
+ );
583
+ })}
584
+ </>
585
+ ) : (
586
+ <motion.div
587
+ initial={{ opacity: 0, y: 20 }}
588
+ animate={{ opacity: 1, y: 0 }}
589
+ className="flex flex-col items-center justify-center text-center px-4 py-8 gap-4"
590
+ >
591
+ {/* Folder icon with pulse */}
592
+ <motion.div
593
+ animate={{ scale: [1, 1.05, 1] }}
594
+ transition={{ repeat: Infinity, duration: 2 }}
595
+ className="w-16 h-16 rounded-xl bg-gradient-to-br from-cyan-900/30 to-cyan-600/10 border border-cyan-500/30 flex items-center justify-center text-cyan-400"
596
+ >
597
+ <Folder size={32} />
598
+ </motion.div>
599
+
600
+ <div>
601
+ <h3 className="text-sm font-black text-zinc-300 mb-2">Welcome to JARVIS</h3>
602
+ <p className="text-xs text-zinc-500 mb-6">Let's get started with your AI coding assistant</p>
603
+ </div>
604
+
605
+ {/* Tutorial steps */}
606
+ <div className="space-y-4 w-full">
607
+ <div className="flex items-start gap-3 text-left">
608
+ <div className="w-6 h-6 rounded-full bg-cyan-600 text-white text-xs flex items-center justify-center font-bold shrink-0 mt-0.5">1</div>
609
+ <div className="flex-1">
610
+ <p className="text-sm text-zinc-300 font-semibold mb-1">Add Workspace</p>
611
+ <p className="text-xs text-zinc-500 leading-relaxed">Click the <span className="text-cyan-400 font-bold">+</span> button on the left to select your project folder</p>
612
+ </div>
613
+ </div>
614
+
615
+ <div className="flex items-start gap-3 text-left">
616
+ <div className="w-6 h-6 rounded-full bg-cyan-600 text-white text-xs flex items-center justify-center font-bold shrink-0 mt-0.5">2</div>
617
+ <div className="flex-1">
618
+ <p className="text-sm text-zinc-300 font-semibold mb-1">Choose Your Folder</p>
619
+ <p className="text-xs text-zinc-500 leading-relaxed">Browse and select your project directory from your computer</p>
620
+ </div>
621
+ </div>
622
+
623
+ <div className="flex items-start gap-3 text-left">
624
+ <div className="w-6 h-6 rounded-full bg-cyan-600 text-white text-xs flex items-center justify-center font-bold shrink-0 mt-0.5">3</div>
625
+ <div className="flex-1">
626
+ <p className="text-sm text-zinc-300 font-semibold mb-1">Start Coding</p>
627
+ <p className="text-xs text-zinc-500 leading-relaxed">Create sessions and let JARVIS help you build amazing things!</p>
628
+ </div>
629
+ </div>
630
+ </div>
631
+
632
+ {/* Add Workspace Button */}
633
+ <button
634
+ onClick={handleAddWorkspace}
635
+ disabled={isAddingWorkspace}
636
+ className={cn(
637
+ "flex items-center justify-center gap-2 w-full px-6 py-3 rounded-xl border-2 border-dashed transition-all cursor-pointer group mt-6",
638
+ isAddingWorkspace
639
+ ? "bg-cyan-900/20 border-cyan-500 text-cyan-400 cursor-wait"
640
+ : "bg-zinc-900/30 border-cyan-500/30 text-cyan-400 hover:bg-cyan-900/20 hover:border-cyan-500"
641
+ )}
642
+ >
643
+ {isAddingWorkspace ? (
644
+ <>
645
+ <Loader2 size={20} className="animate-spin" />
646
+ <span className="text-sm font-bold">Adding...</span>
647
+ </>
648
+ ) : (
649
+ <>
650
+ <Plus size={20} className="group-hover:scale-110 transition-transform" />
651
+ <span className="text-sm font-bold">Add Workspace</span>
652
+ </>
653
+ )}
654
+ </button>
655
+
656
+ {/* Arrow pointing up to button */}
657
+ <motion.div
658
+ animate={{ y: [-5, 0, -5] }}
659
+ transition={{ repeat: Infinity, duration: 1.5 }}
660
+ className="text-cyan-500 self-center mt-2"
661
+ >
662
+ <ChevronDown size={24} className="rotate-180" />
663
+ </motion.div>
664
+ </motion.div>
665
+ )}
666
+ </div>
667
+ </div>
668
+ </motion.div>
669
+
670
+ {/* Context Menu */}
671
+ {contextMenu && (
672
+ <>
673
+ {/* Backdrop to close menu on click outside */}
674
+ <div
675
+ className="fixed inset-0 z-[100]"
676
+ onClick={() => setContextMenu(null)}
677
+ />
678
+
679
+ {/* Context Menu */}
680
+ <div
681
+ className="fixed z-[101] bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl py-1 min-w-[200px] overflow-hidden"
682
+ style={{
683
+ left: `${contextMenu.x}px`,
684
+ top: `${contextMenu.y}px`
685
+ }}
686
+ >
687
+ {/* Menu Header */}
688
+ <div className="px-4 py-2 border-b border-zinc-800">
689
+ <p className="text-[10px] font-bold uppercase tracking-wider text-zinc-500">
690
+ {contextMenu.type === 'workspace' ? 'Workspace' : 'Session'}
691
+ </p>
692
+ <p className="text-xs text-zinc-400 truncate max-w-[180px]">
693
+ {contextMenu.name}
694
+ </p>
695
+ </div>
696
+
697
+ {/* Delete Option */}
698
+ <button
699
+ onClick={handleContextMenuDelete}
700
+ className="w-full px-4 py-2 text-left text-sm text-red-400 hover:bg-red-900/20 transition-colors flex items-center gap-2"
701
+ >
702
+ <Trash2 size={14} />
703
+ Delete {contextMenu.type === 'workspace' ? 'Workspace' : 'Session'}
704
+ </button>
705
+ </div>
706
+ </>
707
+ )}
708
+
709
+ {/* Folder Picker Modal */}
710
+ <AnimatePresence>
711
+ {showFolderModal && (
712
+ <motion.div
713
+ initial={{ opacity: 0 }}
714
+ animate={{ opacity: 1 }}
715
+ exit={{ opacity: 0 }}
716
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
717
+ onClick={() => {
718
+ setShowFolderModal(false);
719
+ setSelectedFolderPath('');
720
+ setSelectedFolderName('');
721
+ }}
722
+ >
723
+ <motion.div
724
+ initial={{ scale: 0.9, y: 10 }}
725
+ animate={{ scale: 1, y: 0 }}
726
+ exit={{ scale: 0.9, y: 10 }}
727
+ className="w-full max-w-md bg-zinc-950 border border-zinc-800 rounded-3xl p-6 shadow-2xl ring-1 ring-white/10"
728
+ onClick={(e) => e.stopPropagation()}
729
+ >
730
+ <div className="flex flex-col items-center text-center space-y-4">
731
+ <div className="w-16 h-16 rounded-full bg-cyan-500/10 text-cyan-500 flex items-center justify-center mb-2">
732
+ <Folder size={32} />
733
+ </div>
734
+ <h3 className="text-xl font-black text-white italic tracking-tighter">Add Workspace</h3>
735
+ <p className="text-xs text-zinc-500 font-medium px-4">
736
+ You're about to add this folder as a workspace
737
+ </p>
738
+
739
+ {/* Folder Info */}
740
+ <div className="w-full bg-zinc-900/50 border border-zinc-800 rounded-xl p-4 space-y-2">
741
+ <div className="flex items-start gap-3">
742
+ <div className="w-8 h-8 rounded-lg bg-cyan-900/20 text-cyan-400 flex items-center justify-center shrink-0 mt-0.5">
743
+ <Folder size={16} />
744
+ </div>
745
+ <div className="flex-1 text-left overflow-hidden">
746
+ <p className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-1">Workspace Name</p>
747
+ <p className="text-sm font-black text-white truncate">{selectedFolderName}</p>
748
+ </div>
749
+ </div>
750
+
751
+ <div className="flex items-start gap-3 pt-2 border-t border-zinc-800">
752
+ <div className="w-8 h-8 rounded-lg bg-zinc-800/50 text-zinc-500 flex items-center justify-center shrink-0 mt-0.5">
753
+ <FolderPlus size={16} />
754
+ </div>
755
+ <div className="flex-1 text-left overflow-hidden">
756
+ <p className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-1">Path</p>
757
+ <p className="text-xs font-mono text-zinc-400 break-all">{selectedFolderPath}</p>
758
+ </div>
759
+ </div>
760
+ </div>
761
+
762
+ <div className="flex items-center gap-3 w-full pt-4">
763
+ <button
764
+ onClick={() => {
765
+ setShowFolderModal(false);
766
+ setSelectedFolderPath('');
767
+ setSelectedFolderName('');
768
+ }}
769
+ disabled={isAddingWorkspace}
770
+ className="flex-1 py-3 rounded-xl bg-zinc-900 text-zinc-500 text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
771
+ >
772
+ Cancel
773
+ </button>
774
+ <button
775
+ onClick={confirmAddWorkspace}
776
+ disabled={isAddingWorkspace}
777
+ className="flex-1 py-3 rounded-xl bg-cyan-600 text-white text-[10px] font-black uppercase tracking-widest hover:bg-cyan-500 transition-all shadow-lg shadow-cyan-900/20 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
778
+ >
779
+ {isAddingWorkspace ? (
780
+ <>
781
+ <Loader2 size={14} className="animate-spin" />
782
+ Adding...
783
+ </>
784
+ ) : (
785
+ 'Add Workspace'
786
+ )}
787
+ </button>
788
+ </div>
789
+ </div>
790
+ </motion.div>
791
+ </motion.div>
792
+ )}
793
+ </AnimatePresence>
794
+
795
+ {/* Delete Confirmation Modal */}
796
+ <AnimatePresence>
797
+ {deleteConfirmModal && (
798
+ <motion.div
799
+ initial={{ opacity: 0 }}
800
+ animate={{ opacity: 1 }}
801
+ exit={{ opacity: 0 }}
802
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
803
+ onClick={() => setDeleteConfirmModal(null)}
804
+ >
805
+ <motion.div
806
+ initial={{ scale: 0.9, y: 10 }}
807
+ animate={{ scale: 1, y: 0 }}
808
+ exit={{ scale: 0.9, y: 10 }}
809
+ className="w-full max-w-sm bg-zinc-950 border border-zinc-800 rounded-3xl p-6 shadow-2xl ring-1 ring-white/10"
810
+ onClick={(e) => e.stopPropagation()}
811
+ >
812
+ <div className="flex flex-col items-center text-center space-y-4">
813
+ <div className="w-12 h-12 rounded-full bg-red-500/10 text-red-500 flex items-center justify-center mb-2">
814
+ <Trash2 size={20} />
815
+ </div>
816
+ <h3 className="text-lg font-black text-white italic tracking-tighter">
817
+ Delete {deleteConfirmModal.type === 'workspace' ? 'Workspace' : 'Session'}?
818
+ </h3>
819
+ <p className="text-xs text-zinc-500 font-medium px-4">
820
+ {deleteConfirmModal.type === 'workspace'
821
+ ? <>This will remove <span className="text-zinc-300 font-bold">"{deleteConfirmModal.name}"</span> but NOT delete any files.</>
822
+ : <>Delete session <span className="text-zinc-300 font-bold">"{deleteConfirmModal.name}"</span>? This action cannot be undone.</>
823
+ }
824
+ </p>
825
+
826
+ <div className="flex items-center gap-3 w-full pt-4">
827
+ <button
828
+ onClick={() => setDeleteConfirmModal(null)}
829
+ className="flex-1 py-3 rounded-xl bg-zinc-900 text-zinc-500 text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all"
830
+ >
831
+ Cancel
832
+ </button>
833
+ <button
834
+ onClick={confirmDelete}
835
+ className="flex-1 py-3 rounded-xl bg-red-600 text-white text-[10px] font-black uppercase tracking-widest hover:bg-red-500 transition-all shadow-lg shadow-red-900/20"
836
+ >
837
+ Delete
838
+ </button>
839
+ </div>
840
+ </div>
841
+ </motion.div>
842
+ </motion.div>
843
+ )}
844
+ </AnimatePresence>
845
+
846
+ {/* Clean Workspaces Confirmation Modal */}
847
+ <AnimatePresence>
848
+ {showCleanConfirm && (
849
+ <motion.div
850
+ initial={{ opacity: 0 }}
851
+ animate={{ opacity: 1 }}
852
+ exit={{ opacity: 0 }}
853
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md"
854
+ onClick={() => setShowCleanConfirm(false)}
855
+ >
856
+ <motion.div
857
+ initial={{ scale: 0.9, y: 10 }}
858
+ animate={{ scale: 1, y: 0 }}
859
+ exit={{ scale: 0.9, y: 10 }}
860
+ className="w-full max-w-sm bg-zinc-950 border border-zinc-800 rounded-3xl p-6 shadow-2xl ring-1 ring-white/10"
861
+ onClick={(e) => e.stopPropagation()}
862
+ >
863
+ <div className="flex flex-col items-center text-center space-y-4">
864
+ <div className="w-12 h-12 rounded-full bg-red-500/10 text-red-500 flex items-center justify-center mb-2">
865
+ <Trash2 size={20} />
866
+ </div>
867
+ <h3 className="text-lg font-black text-white italic tracking-tighter">
868
+ Clean All Workspaces?
869
+ </h3>
870
+ <p className="text-xs text-zinc-500 font-medium px-4">
871
+ This will remove <span className="text-zinc-300 font-bold">all {workspaces.length} workspaces</span> from JARVIS. Your files will NOT be deleted.
872
+ </p>
873
+
874
+ <div className="flex items-center gap-3 w-full pt-4">
875
+ <button
876
+ onClick={() => setShowCleanConfirm(false)}
877
+ className="flex-1 py-3 rounded-xl bg-zinc-900 text-zinc-500 text-[10px] font-black uppercase tracking-widest hover:bg-zinc-800 hover:text-zinc-300 transition-all"
878
+ >
879
+ Cancel
880
+ </button>
881
+ <button
882
+ onClick={confirmCleanWorkspaces}
883
+ className="flex-1 py-3 rounded-xl bg-red-600 text-white text-[10px] font-black uppercase tracking-widest hover:bg-red-500 transition-all shadow-lg shadow-red-900/20"
884
+ >
885
+ Clean All
886
+ </button>
887
+ </div>
888
+ </div>
889
+ </motion.div>
890
+ </motion.div>
891
+ )}
892
+ </AnimatePresence>
893
+ </div>
894
+ );
895
+ }