@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.
- package/dist/cli.js +478 -347
- package/dist/electron/main.js +160 -0
- package/dist/electron/preload.js +19 -0
- package/package.json +19 -6
- 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,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
|
+
}
|