@siteboon/claude-code-ui 1.8.2 → 1.8.4
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/assets/index-CeR_JfKq.js +895 -0
- package/dist/assets/index-Co7ALK3i.css +32 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +6 -1
- package/server/database/auth.db +0 -0
- package/.env.example +0 -12
- package/.nvmrc +0 -1
- package/postcss.config.js +0 -6
- package/src/App.jsx +0 -751
- package/src/components/ChatInterface.jsx +0 -3485
- package/src/components/ClaudeLogo.jsx +0 -11
- package/src/components/ClaudeStatus.jsx +0 -107
- package/src/components/CodeEditor.jsx +0 -422
- package/src/components/CreateTaskModal.jsx +0 -88
- package/src/components/CursorLogo.jsx +0 -9
- package/src/components/DarkModeToggle.jsx +0 -35
- package/src/components/DiffViewer.jsx +0 -41
- package/src/components/ErrorBoundary.jsx +0 -73
- package/src/components/FileTree.jsx +0 -480
- package/src/components/GitPanel.jsx +0 -1283
- package/src/components/ImageViewer.jsx +0 -54
- package/src/components/LoginForm.jsx +0 -110
- package/src/components/MainContent.jsx +0 -577
- package/src/components/MicButton.jsx +0 -272
- package/src/components/MobileNav.jsx +0 -88
- package/src/components/NextTaskBanner.jsx +0 -695
- package/src/components/PRDEditor.jsx +0 -871
- package/src/components/ProtectedRoute.jsx +0 -44
- package/src/components/QuickSettingsPanel.jsx +0 -262
- package/src/components/Settings.jsx +0 -2023
- package/src/components/SetupForm.jsx +0 -135
- package/src/components/Shell.jsx +0 -663
- package/src/components/Sidebar.jsx +0 -1665
- package/src/components/StandaloneShell.jsx +0 -106
- package/src/components/TaskCard.jsx +0 -210
- package/src/components/TaskDetail.jsx +0 -406
- package/src/components/TaskIndicator.jsx +0 -108
- package/src/components/TaskList.jsx +0 -1054
- package/src/components/TaskMasterSetupWizard.jsx +0 -603
- package/src/components/TaskMasterStatus.jsx +0 -86
- package/src/components/TodoList.jsx +0 -91
- package/src/components/Tooltip.jsx +0 -91
- package/src/components/ui/badge.jsx +0 -31
- package/src/components/ui/button.jsx +0 -46
- package/src/components/ui/input.jsx +0 -19
- package/src/components/ui/scroll-area.jsx +0 -23
- package/src/contexts/AuthContext.jsx +0 -158
- package/src/contexts/TaskMasterContext.jsx +0 -324
- package/src/contexts/TasksSettingsContext.jsx +0 -95
- package/src/contexts/ThemeContext.jsx +0 -94
- package/src/contexts/WebSocketContext.jsx +0 -29
- package/src/hooks/useAudioRecorder.js +0 -109
- package/src/hooks/useVersionCheck.js +0 -39
- package/src/index.css +0 -822
- package/src/lib/utils.js +0 -6
- package/src/main.jsx +0 -10
- package/src/utils/api.js +0 -141
- package/src/utils/websocket.js +0 -109
- package/src/utils/whisper.js +0 -37
- package/tailwind.config.js +0 -63
- package/vite.config.js +0 -29
- /package/{public → dist}/convert-icons.md +0 -0
- /package/{public → dist}/favicon.png +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/generate-icons.js +0 -0
- /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
- /package/{public → dist}/icons/cursor.svg +0 -0
- /package/{public → dist}/icons/generate-icons.md +0 -0
- /package/{public → dist}/icons/icon-128x128.png +0 -0
- /package/{public → dist}/icons/icon-128x128.svg +0 -0
- /package/{public → dist}/icons/icon-144x144.png +0 -0
- /package/{public → dist}/icons/icon-144x144.svg +0 -0
- /package/{public → dist}/icons/icon-152x152.png +0 -0
- /package/{public → dist}/icons/icon-152x152.svg +0 -0
- /package/{public → dist}/icons/icon-192x192.png +0 -0
- /package/{public → dist}/icons/icon-192x192.svg +0 -0
- /package/{public → dist}/icons/icon-384x384.png +0 -0
- /package/{public → dist}/icons/icon-384x384.svg +0 -0
- /package/{public → dist}/icons/icon-512x512.png +0 -0
- /package/{public → dist}/icons/icon-512x512.svg +0 -0
- /package/{public → dist}/icons/icon-72x72.png +0 -0
- /package/{public → dist}/icons/icon-72x72.svg +0 -0
- /package/{public → dist}/icons/icon-96x96.png +0 -0
- /package/{public → dist}/icons/icon-96x96.svg +0 -0
- /package/{public → dist}/icons/icon-template.svg +0 -0
- /package/{public → dist}/logo.svg +0 -0
- /package/{public → dist}/manifest.json +0 -0
- /package/{public → dist}/screenshots/cli-selection.png +0 -0
- /package/{public → dist}/screenshots/desktop-main.png +0 -0
- /package/{public → dist}/screenshots/mobile-chat.png +0 -0
- /package/{public → dist}/screenshots/tools-modal.png +0 -0
- /package/{public → dist}/sw.js +0 -0
|
@@ -1,1665 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { ScrollArea } from './ui/scroll-area';
|
|
3
|
-
import { Button } from './ui/button';
|
|
4
|
-
import { Badge } from './ui/badge';
|
|
5
|
-
import { Input } from './ui/input';
|
|
6
|
-
|
|
7
|
-
import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react';
|
|
8
|
-
import { cn } from '../lib/utils';
|
|
9
|
-
import ClaudeLogo from './ClaudeLogo';
|
|
10
|
-
import CursorLogo from './CursorLogo.jsx';
|
|
11
|
-
import TaskIndicator from './TaskIndicator';
|
|
12
|
-
import { api } from '../utils/api';
|
|
13
|
-
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
|
14
|
-
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
|
15
|
-
|
|
16
|
-
// Move formatTimeAgo outside component to avoid recreation on every render
|
|
17
|
-
const formatTimeAgo = (dateString, currentTime) => {
|
|
18
|
-
const date = new Date(dateString);
|
|
19
|
-
const now = currentTime;
|
|
20
|
-
|
|
21
|
-
// Check if date is valid
|
|
22
|
-
if (isNaN(date.getTime())) {
|
|
23
|
-
return 'Unknown';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const diffInMs = now - date;
|
|
27
|
-
const diffInSeconds = Math.floor(diffInMs / 1000);
|
|
28
|
-
const diffInMinutes = Math.floor(diffInMs / (1000 * 60));
|
|
29
|
-
const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60));
|
|
30
|
-
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
|
31
|
-
|
|
32
|
-
if (diffInSeconds < 60) return 'Just now';
|
|
33
|
-
if (diffInMinutes === 1) return '1 min ago';
|
|
34
|
-
if (diffInMinutes < 60) return `${diffInMinutes} mins ago`;
|
|
35
|
-
if (diffInHours === 1) return '1 hour ago';
|
|
36
|
-
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
|
37
|
-
if (diffInDays === 1) return '1 day ago';
|
|
38
|
-
if (diffInDays < 7) return `${diffInDays} days ago`;
|
|
39
|
-
return date.toLocaleDateString();
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
function Sidebar({
|
|
43
|
-
projects,
|
|
44
|
-
selectedProject,
|
|
45
|
-
selectedSession,
|
|
46
|
-
onProjectSelect,
|
|
47
|
-
onSessionSelect,
|
|
48
|
-
onNewSession,
|
|
49
|
-
onSessionDelete,
|
|
50
|
-
onProjectDelete,
|
|
51
|
-
isLoading,
|
|
52
|
-
onRefresh,
|
|
53
|
-
onShowSettings,
|
|
54
|
-
updateAvailable,
|
|
55
|
-
latestVersion,
|
|
56
|
-
currentVersion,
|
|
57
|
-
onShowVersionModal,
|
|
58
|
-
isPWA,
|
|
59
|
-
isMobile
|
|
60
|
-
}) {
|
|
61
|
-
const [expandedProjects, setExpandedProjects] = useState(new Set());
|
|
62
|
-
const [editingProject, setEditingProject] = useState(null);
|
|
63
|
-
const [showNewProject, setShowNewProject] = useState(false);
|
|
64
|
-
const [editingName, setEditingName] = useState('');
|
|
65
|
-
const [newProjectPath, setNewProjectPath] = useState('');
|
|
66
|
-
const [creatingProject, setCreatingProject] = useState(false);
|
|
67
|
-
const [loadingSessions, setLoadingSessions] = useState({});
|
|
68
|
-
const [additionalSessions, setAdditionalSessions] = useState({});
|
|
69
|
-
const [initialSessionsLoaded, setInitialSessionsLoaded] = useState(new Set());
|
|
70
|
-
const [currentTime, setCurrentTime] = useState(new Date());
|
|
71
|
-
const [projectSortOrder, setProjectSortOrder] = useState('name');
|
|
72
|
-
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
73
|
-
const [editingSession, setEditingSession] = useState(null);
|
|
74
|
-
const [editingSessionName, setEditingSessionName] = useState('');
|
|
75
|
-
const [generatingSummary, setGeneratingSummary] = useState({});
|
|
76
|
-
const [searchFilter, setSearchFilter] = useState('');
|
|
77
|
-
const [showPathDropdown, setShowPathDropdown] = useState(false);
|
|
78
|
-
const [pathList, setPathList] = useState([]);
|
|
79
|
-
const [filteredPaths, setFilteredPaths] = useState([]);
|
|
80
|
-
const [selectedPathIndex, setSelectedPathIndex] = useState(-1);
|
|
81
|
-
|
|
82
|
-
// TaskMaster context
|
|
83
|
-
const { setCurrentProject, mcpServerStatus } = useTaskMaster();
|
|
84
|
-
const { tasksEnabled } = useTasksSettings();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Starred projects state - persisted in localStorage
|
|
88
|
-
const [starredProjects, setStarredProjects] = useState(() => {
|
|
89
|
-
try {
|
|
90
|
-
const saved = localStorage.getItem('starredProjects');
|
|
91
|
-
return saved ? new Set(JSON.parse(saved)) : new Set();
|
|
92
|
-
} catch (error) {
|
|
93
|
-
console.error('Error loading starred projects:', error);
|
|
94
|
-
return new Set();
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Touch handler to prevent double-tap issues on iPad (only for buttons, not scroll areas)
|
|
99
|
-
const handleTouchClick = (callback) => {
|
|
100
|
-
return (e) => {
|
|
101
|
-
// Only prevent default for buttons/clickable elements, not scrollable areas
|
|
102
|
-
if (e.target.closest('.overflow-y-auto') || e.target.closest('[data-scroll-container]')) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
e.stopPropagation();
|
|
107
|
-
callback();
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// Auto-update timestamps every minute
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
const timer = setInterval(() => {
|
|
114
|
-
setCurrentTime(new Date());
|
|
115
|
-
}, 60000); // Update every 60 seconds
|
|
116
|
-
|
|
117
|
-
return () => clearInterval(timer);
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
// Clear additional sessions when projects list changes (e.g., after refresh)
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
setAdditionalSessions({});
|
|
123
|
-
setInitialSessionsLoaded(new Set());
|
|
124
|
-
}, [projects]);
|
|
125
|
-
|
|
126
|
-
// Auto-expand project folder when a session is selected
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
if (selectedSession && selectedProject) {
|
|
129
|
-
setExpandedProjects(prev => new Set([...prev, selectedProject.name]));
|
|
130
|
-
}
|
|
131
|
-
}, [selectedSession, selectedProject]);
|
|
132
|
-
|
|
133
|
-
// Mark sessions as loaded when projects come in
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (projects.length > 0 && !isLoading) {
|
|
136
|
-
const newLoaded = new Set();
|
|
137
|
-
projects.forEach(project => {
|
|
138
|
-
if (project.sessions && project.sessions.length >= 0) {
|
|
139
|
-
newLoaded.add(project.name);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
setInitialSessionsLoaded(newLoaded);
|
|
143
|
-
}
|
|
144
|
-
}, [projects, isLoading]);
|
|
145
|
-
|
|
146
|
-
// Load project sort order from settings
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
const loadSortOrder = () => {
|
|
149
|
-
try {
|
|
150
|
-
const savedSettings = localStorage.getItem('claude-settings');
|
|
151
|
-
if (savedSettings) {
|
|
152
|
-
const settings = JSON.parse(savedSettings);
|
|
153
|
-
setProjectSortOrder(settings.projectSortOrder || 'name');
|
|
154
|
-
}
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error('Error loading sort order:', error);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// Load initially
|
|
161
|
-
loadSortOrder();
|
|
162
|
-
|
|
163
|
-
// Listen for storage changes
|
|
164
|
-
const handleStorageChange = (e) => {
|
|
165
|
-
if (e.key === 'claude-settings') {
|
|
166
|
-
loadSortOrder();
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
window.addEventListener('storage', handleStorageChange);
|
|
171
|
-
|
|
172
|
-
// Also check periodically when component is focused (for same-tab changes)
|
|
173
|
-
const checkInterval = setInterval(() => {
|
|
174
|
-
if (document.hasFocus()) {
|
|
175
|
-
loadSortOrder();
|
|
176
|
-
}
|
|
177
|
-
}, 1000);
|
|
178
|
-
|
|
179
|
-
return () => {
|
|
180
|
-
window.removeEventListener('storage', handleStorageChange);
|
|
181
|
-
clearInterval(checkInterval);
|
|
182
|
-
};
|
|
183
|
-
}, []);
|
|
184
|
-
|
|
185
|
-
// Load available paths for suggestions
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
const loadPaths = async () => {
|
|
188
|
-
try {
|
|
189
|
-
// Get recent paths from localStorage
|
|
190
|
-
const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]');
|
|
191
|
-
|
|
192
|
-
// Load common/home directory paths
|
|
193
|
-
const response = await api.browseFilesystem();
|
|
194
|
-
const data = await response.json();
|
|
195
|
-
|
|
196
|
-
if (data.suggestions) {
|
|
197
|
-
const homePaths = data.suggestions.map(s => ({ name: s.name, path: s.path }));
|
|
198
|
-
const allPaths = [...recentPaths.map(path => ({ name: path.split('/').pop(), path })), ...homePaths];
|
|
199
|
-
setPathList(allPaths);
|
|
200
|
-
} else {
|
|
201
|
-
setPathList(recentPaths.map(path => ({ name: path.split('/').pop(), path })));
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error('Error loading paths:', error);
|
|
205
|
-
const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]');
|
|
206
|
-
setPathList(recentPaths.map(path => ({ name: path.split('/').pop(), path })));
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
loadPaths();
|
|
211
|
-
}, []);
|
|
212
|
-
|
|
213
|
-
// Handle input change and path filtering with dynamic browsing (ChatInterface pattern + dynamic browsing)
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
const inputValue = newProjectPath.trim();
|
|
216
|
-
|
|
217
|
-
if (inputValue.length === 0) {
|
|
218
|
-
setShowPathDropdown(false);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Show dropdown when user starts typing
|
|
223
|
-
setShowPathDropdown(true);
|
|
224
|
-
|
|
225
|
-
const updateSuggestions = async () => {
|
|
226
|
-
// First show filtered existing suggestions from pathList
|
|
227
|
-
const staticFiltered = pathList.filter(pathItem =>
|
|
228
|
-
pathItem.name.toLowerCase().includes(inputValue.toLowerCase()) ||
|
|
229
|
-
pathItem.path.toLowerCase().includes(inputValue.toLowerCase())
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
// Check if input looks like a directory path for dynamic browsing
|
|
233
|
-
const isDirPath = inputValue.includes('/') && inputValue.length > 1;
|
|
234
|
-
|
|
235
|
-
if (isDirPath) {
|
|
236
|
-
try {
|
|
237
|
-
let dirToSearch;
|
|
238
|
-
|
|
239
|
-
// Determine which directory to search
|
|
240
|
-
if (inputValue.endsWith('/')) {
|
|
241
|
-
// User typed "/home/simos/" - search inside /home/simos
|
|
242
|
-
dirToSearch = inputValue.slice(0, -1);
|
|
243
|
-
} else {
|
|
244
|
-
// User typed "/home/simos/con" - search inside /home/simos for items starting with "con"
|
|
245
|
-
const lastSlashIndex = inputValue.lastIndexOf('/');
|
|
246
|
-
dirToSearch = inputValue.substring(0, lastSlashIndex);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Only search if we have a valid directory path (not root only)
|
|
250
|
-
if (dirToSearch && dirToSearch !== '') {
|
|
251
|
-
const response = await api.browseFilesystem(dirToSearch);
|
|
252
|
-
const data = await response.json();
|
|
253
|
-
|
|
254
|
-
if (data.suggestions) {
|
|
255
|
-
// Filter directories that match the current input
|
|
256
|
-
const partialName = inputValue.substring(inputValue.lastIndexOf('/') + 1);
|
|
257
|
-
const dynamicPaths = data.suggestions
|
|
258
|
-
.filter(suggestion => {
|
|
259
|
-
const dirName = suggestion.name;
|
|
260
|
-
return partialName ? dirName.toLowerCase().startsWith(partialName.toLowerCase()) : true;
|
|
261
|
-
})
|
|
262
|
-
.map(s => ({ name: s.name, path: s.path }))
|
|
263
|
-
.slice(0, 8);
|
|
264
|
-
|
|
265
|
-
// Combine static and dynamic suggestions, prioritize dynamic
|
|
266
|
-
const combined = [...dynamicPaths, ...staticFiltered].slice(0, 8);
|
|
267
|
-
setFilteredPaths(combined);
|
|
268
|
-
setSelectedPathIndex(-1);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
} catch (error) {
|
|
273
|
-
console.debug('Dynamic browsing failed:', error.message);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Fallback to just static filtered suggestions
|
|
278
|
-
setFilteredPaths(staticFiltered.slice(0, 8));
|
|
279
|
-
setSelectedPathIndex(-1);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
updateSuggestions();
|
|
283
|
-
}, [newProjectPath, pathList]);
|
|
284
|
-
|
|
285
|
-
// Select path from dropdown (ChatInterface pattern)
|
|
286
|
-
const selectPath = (pathItem) => {
|
|
287
|
-
setNewProjectPath(pathItem.path);
|
|
288
|
-
setShowPathDropdown(false);
|
|
289
|
-
setSelectedPathIndex(-1);
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// Save path to recent paths
|
|
293
|
-
const saveToRecentPaths = (path) => {
|
|
294
|
-
try {
|
|
295
|
-
const recentPaths = JSON.parse(localStorage.getItem('recentProjectPaths') || '[]');
|
|
296
|
-
const updatedPaths = [path, ...recentPaths.filter(p => p !== path)].slice(0, 10);
|
|
297
|
-
localStorage.setItem('recentProjectPaths', JSON.stringify(updatedPaths));
|
|
298
|
-
} catch (error) {
|
|
299
|
-
console.error('Error saving recent paths:', error);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
const toggleProject = (projectName) => {
|
|
304
|
-
const newExpanded = new Set(expandedProjects);
|
|
305
|
-
if (newExpanded.has(projectName)) {
|
|
306
|
-
newExpanded.delete(projectName);
|
|
307
|
-
} else {
|
|
308
|
-
newExpanded.add(projectName);
|
|
309
|
-
}
|
|
310
|
-
setExpandedProjects(newExpanded);
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// Starred projects utility functions
|
|
314
|
-
const toggleStarProject = (projectName) => {
|
|
315
|
-
const newStarred = new Set(starredProjects);
|
|
316
|
-
if (newStarred.has(projectName)) {
|
|
317
|
-
newStarred.delete(projectName);
|
|
318
|
-
} else {
|
|
319
|
-
newStarred.add(projectName);
|
|
320
|
-
}
|
|
321
|
-
setStarredProjects(newStarred);
|
|
322
|
-
|
|
323
|
-
// Persist to localStorage
|
|
324
|
-
try {
|
|
325
|
-
localStorage.setItem('starredProjects', JSON.stringify([...newStarred]));
|
|
326
|
-
} catch (error) {
|
|
327
|
-
console.error('Error saving starred projects:', error);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const isProjectStarred = (projectName) => {
|
|
332
|
-
return starredProjects.has(projectName);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
// Helper function to get all sessions for a project (initial + additional)
|
|
336
|
-
const getAllSessions = (project) => {
|
|
337
|
-
// Combine Claude and Cursor sessions; Sidebar will display icon per row
|
|
338
|
-
const claudeSessions = [...(project.sessions || []), ...(additionalSessions[project.name] || [])].map(s => ({ ...s, __provider: 'claude' }));
|
|
339
|
-
const cursorSessions = (project.cursorSessions || []).map(s => ({ ...s, __provider: 'cursor' }));
|
|
340
|
-
// Sort by most recent activity/date
|
|
341
|
-
const normalizeDate = (s) => new Date(s.__provider === 'cursor' ? s.createdAt : s.lastActivity);
|
|
342
|
-
return [...claudeSessions, ...cursorSessions].sort((a, b) => normalizeDate(b) - normalizeDate(a));
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// Helper function to get the last activity date for a project
|
|
346
|
-
const getProjectLastActivity = (project) => {
|
|
347
|
-
const allSessions = getAllSessions(project);
|
|
348
|
-
if (allSessions.length === 0) {
|
|
349
|
-
return new Date(0); // Return epoch date for projects with no sessions
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Find the most recent session activity
|
|
353
|
-
const mostRecentDate = allSessions.reduce((latest, session) => {
|
|
354
|
-
const sessionDate = new Date(session.lastActivity);
|
|
355
|
-
return sessionDate > latest ? sessionDate : latest;
|
|
356
|
-
}, new Date(0));
|
|
357
|
-
|
|
358
|
-
return mostRecentDate;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
// Combined sorting: starred projects first, then by selected order
|
|
362
|
-
const sortedProjects = [...projects].sort((a, b) => {
|
|
363
|
-
const aStarred = isProjectStarred(a.name);
|
|
364
|
-
const bStarred = isProjectStarred(b.name);
|
|
365
|
-
|
|
366
|
-
// First, sort by starred status
|
|
367
|
-
if (aStarred && !bStarred) return -1;
|
|
368
|
-
if (!aStarred && bStarred) return 1;
|
|
369
|
-
|
|
370
|
-
// For projects with same starred status, sort by selected order
|
|
371
|
-
if (projectSortOrder === 'date') {
|
|
372
|
-
// Sort by most recent activity (descending)
|
|
373
|
-
return getProjectLastActivity(b) - getProjectLastActivity(a);
|
|
374
|
-
} else {
|
|
375
|
-
// Sort by display name (user-defined) or fallback to name (ascending)
|
|
376
|
-
const nameA = a.displayName || a.name;
|
|
377
|
-
const nameB = b.displayName || b.name;
|
|
378
|
-
return nameA.localeCompare(nameB);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
const startEditing = (project) => {
|
|
383
|
-
setEditingProject(project.name);
|
|
384
|
-
setEditingName(project.displayName);
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const cancelEditing = () => {
|
|
388
|
-
setEditingProject(null);
|
|
389
|
-
setEditingName('');
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const saveProjectName = async (projectName) => {
|
|
393
|
-
try {
|
|
394
|
-
const response = await api.renameProject(projectName, editingName);
|
|
395
|
-
|
|
396
|
-
if (response.ok) {
|
|
397
|
-
// Refresh projects to get updated data
|
|
398
|
-
if (window.refreshProjects) {
|
|
399
|
-
window.refreshProjects();
|
|
400
|
-
} else {
|
|
401
|
-
window.location.reload();
|
|
402
|
-
}
|
|
403
|
-
} else {
|
|
404
|
-
console.error('Failed to rename project');
|
|
405
|
-
}
|
|
406
|
-
} catch (error) {
|
|
407
|
-
console.error('Error renaming project:', error);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
setEditingProject(null);
|
|
411
|
-
setEditingName('');
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const deleteSession = async (projectName, sessionId) => {
|
|
415
|
-
if (!confirm('Are you sure you want to delete this session? This action cannot be undone.')) {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
const response = await api.deleteSession(projectName, sessionId);
|
|
421
|
-
|
|
422
|
-
if (response.ok) {
|
|
423
|
-
// Call parent callback if provided
|
|
424
|
-
if (onSessionDelete) {
|
|
425
|
-
onSessionDelete(sessionId);
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
console.error('Failed to delete session');
|
|
429
|
-
alert('Failed to delete session. Please try again.');
|
|
430
|
-
}
|
|
431
|
-
} catch (error) {
|
|
432
|
-
console.error('Error deleting session:', error);
|
|
433
|
-
alert('Error deleting session. Please try again.');
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const deleteProject = async (projectName) => {
|
|
438
|
-
if (!confirm('Are you sure you want to delete this empty project? This action cannot be undone.')) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
const response = await api.deleteProject(projectName);
|
|
444
|
-
|
|
445
|
-
if (response.ok) {
|
|
446
|
-
// Call parent callback if provided
|
|
447
|
-
if (onProjectDelete) {
|
|
448
|
-
onProjectDelete(projectName);
|
|
449
|
-
}
|
|
450
|
-
} else {
|
|
451
|
-
const error = await response.json();
|
|
452
|
-
console.error('Failed to delete project');
|
|
453
|
-
alert(error.error || 'Failed to delete project. Please try again.');
|
|
454
|
-
}
|
|
455
|
-
} catch (error) {
|
|
456
|
-
console.error('Error deleting project:', error);
|
|
457
|
-
alert('Error deleting project. Please try again.');
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const createNewProject = async () => {
|
|
462
|
-
if (!newProjectPath.trim()) {
|
|
463
|
-
alert('Please enter a project path');
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
setCreatingProject(true);
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
const response = await api.createProject(newProjectPath.trim());
|
|
471
|
-
|
|
472
|
-
if (response.ok) {
|
|
473
|
-
const result = await response.json();
|
|
474
|
-
|
|
475
|
-
// Save the path to recent paths before clearing
|
|
476
|
-
saveToRecentPaths(newProjectPath.trim());
|
|
477
|
-
|
|
478
|
-
setShowNewProject(false);
|
|
479
|
-
setNewProjectPath('');
|
|
480
|
-
setShowSuggestions(false);
|
|
481
|
-
|
|
482
|
-
// Refresh projects to show the new one
|
|
483
|
-
if (window.refreshProjects) {
|
|
484
|
-
window.refreshProjects();
|
|
485
|
-
} else {
|
|
486
|
-
window.location.reload();
|
|
487
|
-
}
|
|
488
|
-
} else {
|
|
489
|
-
const error = await response.json();
|
|
490
|
-
alert(error.error || 'Failed to create project. Please try again.');
|
|
491
|
-
}
|
|
492
|
-
} catch (error) {
|
|
493
|
-
console.error('Error creating project:', error);
|
|
494
|
-
alert('Error creating project. Please try again.');
|
|
495
|
-
} finally {
|
|
496
|
-
setCreatingProject(false);
|
|
497
|
-
}
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
const cancelNewProject = () => {
|
|
501
|
-
setShowNewProject(false);
|
|
502
|
-
setNewProjectPath('');
|
|
503
|
-
setShowSuggestions(false);
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
const loadMoreSessions = async (project) => {
|
|
507
|
-
// Check if we can load more sessions
|
|
508
|
-
const canLoadMore = project.sessionMeta?.hasMore !== false;
|
|
509
|
-
|
|
510
|
-
if (!canLoadMore || loadingSessions[project.name]) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
setLoadingSessions(prev => ({ ...prev, [project.name]: true }));
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
const currentSessionCount = (project.sessions?.length || 0) + (additionalSessions[project.name]?.length || 0);
|
|
518
|
-
const response = await api.sessions(project.name, 5, currentSessionCount);
|
|
519
|
-
|
|
520
|
-
if (response.ok) {
|
|
521
|
-
const result = await response.json();
|
|
522
|
-
|
|
523
|
-
// Store additional sessions locally
|
|
524
|
-
setAdditionalSessions(prev => ({
|
|
525
|
-
...prev,
|
|
526
|
-
[project.name]: [
|
|
527
|
-
...(prev[project.name] || []),
|
|
528
|
-
...result.sessions
|
|
529
|
-
]
|
|
530
|
-
}));
|
|
531
|
-
|
|
532
|
-
// Update project metadata if needed
|
|
533
|
-
if (result.hasMore === false) {
|
|
534
|
-
// Mark that there are no more sessions to load
|
|
535
|
-
project.sessionMeta = { ...project.sessionMeta, hasMore: false };
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
console.error('Error loading more sessions:', error);
|
|
540
|
-
} finally {
|
|
541
|
-
setLoadingSessions(prev => ({ ...prev, [project.name]: false }));
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
// Filter projects based on search input
|
|
546
|
-
const filteredProjects = sortedProjects.filter(project => {
|
|
547
|
-
if (!searchFilter.trim()) return true;
|
|
548
|
-
|
|
549
|
-
const searchLower = searchFilter.toLowerCase();
|
|
550
|
-
const displayName = (project.displayName || project.name).toLowerCase();
|
|
551
|
-
const projectName = project.name.toLowerCase();
|
|
552
|
-
|
|
553
|
-
// Search in both display name and actual project name/path
|
|
554
|
-
return displayName.includes(searchLower) || projectName.includes(searchLower);
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// Enhanced project selection that updates both the main UI and TaskMaster context
|
|
558
|
-
const handleProjectSelect = (project) => {
|
|
559
|
-
// Call the original project select handler
|
|
560
|
-
onProjectSelect(project);
|
|
561
|
-
|
|
562
|
-
// Update TaskMaster context with the selected project
|
|
563
|
-
setCurrentProject(project);
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
return (
|
|
567
|
-
<div
|
|
568
|
-
className="h-full flex flex-col bg-card md:select-none"
|
|
569
|
-
style={isPWA && isMobile ? { paddingTop: '44px' } : {}}
|
|
570
|
-
>
|
|
571
|
-
{/* Header */}
|
|
572
|
-
<div className="md:p-4 md:border-b md:border-border">
|
|
573
|
-
{/* Desktop Header */}
|
|
574
|
-
<div className="hidden md:flex items-center justify-between">
|
|
575
|
-
<div className="flex items-center gap-3">
|
|
576
|
-
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center shadow-sm">
|
|
577
|
-
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
578
|
-
</div>
|
|
579
|
-
<div>
|
|
580
|
-
<h1 className="text-lg font-bold text-foreground">Claude Code UI</h1>
|
|
581
|
-
<p className="text-sm text-muted-foreground">AI coding assistant interface</p>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
<div className="flex gap-2">
|
|
585
|
-
<Button
|
|
586
|
-
variant="ghost"
|
|
587
|
-
size="sm"
|
|
588
|
-
className="h-9 w-9 px-0 hover:bg-accent transition-colors duration-200 group"
|
|
589
|
-
onClick={async () => {
|
|
590
|
-
setIsRefreshing(true);
|
|
591
|
-
try {
|
|
592
|
-
await onRefresh();
|
|
593
|
-
} finally {
|
|
594
|
-
setIsRefreshing(false);
|
|
595
|
-
}
|
|
596
|
-
}}
|
|
597
|
-
disabled={isRefreshing}
|
|
598
|
-
title="Refresh projects and sessions (Ctrl+R)"
|
|
599
|
-
>
|
|
600
|
-
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''} group-hover:rotate-180 transition-transform duration-300`} />
|
|
601
|
-
</Button>
|
|
602
|
-
<Button
|
|
603
|
-
variant="default"
|
|
604
|
-
size="sm"
|
|
605
|
-
className="h-9 w-9 px-0 bg-primary hover:bg-primary/90 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
606
|
-
onClick={() => setShowNewProject(true)}
|
|
607
|
-
title="Create new project (Ctrl+N)"
|
|
608
|
-
>
|
|
609
|
-
<FolderPlus className="w-4 h-4" />
|
|
610
|
-
</Button>
|
|
611
|
-
</div>
|
|
612
|
-
</div>
|
|
613
|
-
|
|
614
|
-
{/* Mobile Header */}
|
|
615
|
-
<div
|
|
616
|
-
className="md:hidden p-3 border-b border-border"
|
|
617
|
-
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
|
618
|
-
>
|
|
619
|
-
<div className="flex items-center justify-between">
|
|
620
|
-
<div className="flex items-center gap-3">
|
|
621
|
-
<div className="w-8 h-8 bg-primary rounded-lg flex items-center justify-center">
|
|
622
|
-
<MessageSquare className="w-4 h-4 text-primary-foreground" />
|
|
623
|
-
</div>
|
|
624
|
-
<div>
|
|
625
|
-
<h1 className="text-lg font-semibold text-foreground">Claude Code UI</h1>
|
|
626
|
-
<p className="text-sm text-muted-foreground">Projects</p>
|
|
627
|
-
</div>
|
|
628
|
-
</div>
|
|
629
|
-
<div className="flex gap-2">
|
|
630
|
-
<button
|
|
631
|
-
className="w-8 h-8 rounded-md bg-background border border-border flex items-center justify-center active:scale-95 transition-all duration-150"
|
|
632
|
-
onClick={async () => {
|
|
633
|
-
setIsRefreshing(true);
|
|
634
|
-
try {
|
|
635
|
-
await onRefresh();
|
|
636
|
-
} finally {
|
|
637
|
-
setIsRefreshing(false);
|
|
638
|
-
}
|
|
639
|
-
}}
|
|
640
|
-
disabled={isRefreshing}
|
|
641
|
-
>
|
|
642
|
-
<RefreshCw className={`w-4 h-4 text-foreground ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
643
|
-
</button>
|
|
644
|
-
<button
|
|
645
|
-
className="w-8 h-8 rounded-md bg-primary text-primary-foreground flex items-center justify-center active:scale-95 transition-all duration-150"
|
|
646
|
-
onClick={() => setShowNewProject(true)}
|
|
647
|
-
>
|
|
648
|
-
<FolderPlus className="w-4 h-4" />
|
|
649
|
-
</button>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
</div>
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
{/* New Project Form */}
|
|
657
|
-
{showNewProject && (
|
|
658
|
-
<div className="md:p-3 md:border-b md:border-border md:bg-muted/30">
|
|
659
|
-
{/* Desktop Form */}
|
|
660
|
-
<div className="hidden md:block space-y-2">
|
|
661
|
-
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
662
|
-
<FolderPlus className="w-4 h-4" />
|
|
663
|
-
Create New Project
|
|
664
|
-
</div>
|
|
665
|
-
<div className="relative">
|
|
666
|
-
<Input
|
|
667
|
-
value={newProjectPath}
|
|
668
|
-
onChange={(e) => setNewProjectPath(e.target.value)}
|
|
669
|
-
placeholder="/path/to/project or relative/path"
|
|
670
|
-
className="text-sm focus:ring-2 focus:ring-primary/20"
|
|
671
|
-
autoFocus
|
|
672
|
-
onKeyDown={(e) => {
|
|
673
|
-
// Handle path dropdown navigation (ChatInterface pattern)
|
|
674
|
-
if (showPathDropdown && filteredPaths.length > 0) {
|
|
675
|
-
if (e.key === 'ArrowDown') {
|
|
676
|
-
e.preventDefault();
|
|
677
|
-
setSelectedPathIndex(prev =>
|
|
678
|
-
prev < filteredPaths.length - 1 ? prev + 1 : 0
|
|
679
|
-
);
|
|
680
|
-
} else if (e.key === 'ArrowUp') {
|
|
681
|
-
e.preventDefault();
|
|
682
|
-
setSelectedPathIndex(prev =>
|
|
683
|
-
prev > 0 ? prev - 1 : filteredPaths.length - 1
|
|
684
|
-
);
|
|
685
|
-
} else if (e.key === 'Enter') {
|
|
686
|
-
e.preventDefault();
|
|
687
|
-
if (selectedPathIndex >= 0) {
|
|
688
|
-
selectPath(filteredPaths[selectedPathIndex]);
|
|
689
|
-
} else if (filteredPaths.length > 0) {
|
|
690
|
-
selectPath(filteredPaths[0]);
|
|
691
|
-
} else {
|
|
692
|
-
createNewProject();
|
|
693
|
-
}
|
|
694
|
-
return;
|
|
695
|
-
} else if (e.key === 'Escape') {
|
|
696
|
-
e.preventDefault();
|
|
697
|
-
setShowPathDropdown(false);
|
|
698
|
-
return;
|
|
699
|
-
} else if (e.key === 'Tab') {
|
|
700
|
-
e.preventDefault();
|
|
701
|
-
if (selectedPathIndex >= 0) {
|
|
702
|
-
selectPath(filteredPaths[selectedPathIndex]);
|
|
703
|
-
} else if (filteredPaths.length > 0) {
|
|
704
|
-
selectPath(filteredPaths[0]);
|
|
705
|
-
}
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Regular input handling
|
|
711
|
-
if (e.key === 'Enter') {
|
|
712
|
-
createNewProject();
|
|
713
|
-
}
|
|
714
|
-
if (e.key === 'Escape') {
|
|
715
|
-
cancelNewProject();
|
|
716
|
-
}
|
|
717
|
-
}}
|
|
718
|
-
/>
|
|
719
|
-
|
|
720
|
-
{/* Path dropdown (ChatInterface pattern) */}
|
|
721
|
-
{showPathDropdown && filteredPaths.length > 0 && (
|
|
722
|
-
<div className="absolute top-full left-0 right-0 mt-1 bg-popover border border-border rounded-md shadow-lg max-h-48 overflow-y-auto z-50">
|
|
723
|
-
{filteredPaths.map((pathItem, index) => (
|
|
724
|
-
<div
|
|
725
|
-
key={pathItem.path}
|
|
726
|
-
className={`px-3 py-2 cursor-pointer border-b border-border last:border-b-0 ${
|
|
727
|
-
index === selectedPathIndex
|
|
728
|
-
? 'bg-accent text-accent-foreground'
|
|
729
|
-
: 'hover:bg-accent/50'
|
|
730
|
-
}`}
|
|
731
|
-
onMouseDown={(e) => {
|
|
732
|
-
e.preventDefault();
|
|
733
|
-
e.stopPropagation();
|
|
734
|
-
}}
|
|
735
|
-
onClick={(e) => {
|
|
736
|
-
e.preventDefault();
|
|
737
|
-
e.stopPropagation();
|
|
738
|
-
selectPath(pathItem);
|
|
739
|
-
}}
|
|
740
|
-
>
|
|
741
|
-
<div className="flex items-center gap-2">
|
|
742
|
-
<Folder className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
|
743
|
-
<div>
|
|
744
|
-
<div className="font-medium text-sm">{pathItem.name}</div>
|
|
745
|
-
<div className="text-xs text-muted-foreground font-mono">
|
|
746
|
-
{pathItem.path}
|
|
747
|
-
</div>
|
|
748
|
-
</div>
|
|
749
|
-
</div>
|
|
750
|
-
</div>
|
|
751
|
-
))}
|
|
752
|
-
</div>
|
|
753
|
-
)}
|
|
754
|
-
</div>
|
|
755
|
-
<div className="flex gap-2">
|
|
756
|
-
<Button
|
|
757
|
-
size="sm"
|
|
758
|
-
onClick={createNewProject}
|
|
759
|
-
disabled={!newProjectPath.trim() || creatingProject}
|
|
760
|
-
className="flex-1 h-8 text-xs hover:bg-primary/90 transition-colors"
|
|
761
|
-
>
|
|
762
|
-
{creatingProject ? 'Creating...' : 'Create Project'}
|
|
763
|
-
</Button>
|
|
764
|
-
<Button
|
|
765
|
-
size="sm"
|
|
766
|
-
variant="outline"
|
|
767
|
-
onClick={cancelNewProject}
|
|
768
|
-
disabled={creatingProject}
|
|
769
|
-
className="h-8 text-xs hover:bg-accent transition-colors"
|
|
770
|
-
>
|
|
771
|
-
Cancel
|
|
772
|
-
</Button>
|
|
773
|
-
</div>
|
|
774
|
-
</div>
|
|
775
|
-
|
|
776
|
-
{/* Mobile Form - Simple Overlay */}
|
|
777
|
-
<div className="md:hidden fixed inset-0 z-[70] bg-black/50 backdrop-blur-sm flex items-end justify-center px-4 pb-24">
|
|
778
|
-
<div className="w-full max-w-sm bg-card rounded-t-lg border-t border-border p-4 space-y-4 animate-in slide-in-from-bottom duration-300">
|
|
779
|
-
<div className="flex items-center justify-between">
|
|
780
|
-
<div className="flex items-center gap-2">
|
|
781
|
-
<div className="w-6 h-6 bg-primary/10 rounded-md flex items-center justify-center">
|
|
782
|
-
<FolderPlus className="w-3 h-3 text-primary" />
|
|
783
|
-
</div>
|
|
784
|
-
<div>
|
|
785
|
-
<h2 className="text-base font-semibold text-foreground">New Project</h2>
|
|
786
|
-
</div>
|
|
787
|
-
</div>
|
|
788
|
-
<button
|
|
789
|
-
onClick={cancelNewProject}
|
|
790
|
-
disabled={creatingProject}
|
|
791
|
-
className="w-6 h-6 rounded-md bg-muted flex items-center justify-center active:scale-95 transition-transform"
|
|
792
|
-
>
|
|
793
|
-
<X className="w-3 h-3" />
|
|
794
|
-
</button>
|
|
795
|
-
</div>
|
|
796
|
-
|
|
797
|
-
<div className="space-y-3">
|
|
798
|
-
<div className="relative">
|
|
799
|
-
<Input
|
|
800
|
-
value={newProjectPath}
|
|
801
|
-
onChange={(e) => setNewProjectPath(e.target.value)}
|
|
802
|
-
placeholder="/path/to/project or relative/path"
|
|
803
|
-
className="text-sm h-10 rounded-md focus:border-primary transition-colors"
|
|
804
|
-
autoFocus
|
|
805
|
-
onKeyDown={(e) => {
|
|
806
|
-
// Handle path dropdown navigation (same as desktop)
|
|
807
|
-
if (showPathDropdown && filteredPaths.length > 0) {
|
|
808
|
-
if (e.key === 'ArrowDown') {
|
|
809
|
-
e.preventDefault();
|
|
810
|
-
setSelectedPathIndex(prev =>
|
|
811
|
-
prev < filteredPaths.length - 1 ? prev + 1 : 0
|
|
812
|
-
);
|
|
813
|
-
} else if (e.key === 'ArrowUp') {
|
|
814
|
-
e.preventDefault();
|
|
815
|
-
setSelectedPathIndex(prev =>
|
|
816
|
-
prev > 0 ? prev - 1 : filteredPaths.length - 1
|
|
817
|
-
);
|
|
818
|
-
} else if (e.key === 'Enter') {
|
|
819
|
-
e.preventDefault();
|
|
820
|
-
if (selectedPathIndex >= 0) {
|
|
821
|
-
selectPath(filteredPaths[selectedPathIndex]);
|
|
822
|
-
} else if (filteredPaths.length > 0) {
|
|
823
|
-
selectPath(filteredPaths[0]);
|
|
824
|
-
} else {
|
|
825
|
-
createNewProject();
|
|
826
|
-
}
|
|
827
|
-
return;
|
|
828
|
-
} else if (e.key === 'Escape') {
|
|
829
|
-
e.preventDefault();
|
|
830
|
-
setShowPathDropdown(false);
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Regular input handling
|
|
836
|
-
if (e.key === 'Enter') {
|
|
837
|
-
createNewProject();
|
|
838
|
-
}
|
|
839
|
-
if (e.key === 'Escape') {
|
|
840
|
-
cancelNewProject();
|
|
841
|
-
}
|
|
842
|
-
}}
|
|
843
|
-
style={{
|
|
844
|
-
fontSize: '16px', // Prevents zoom on iOS
|
|
845
|
-
WebkitAppearance: 'none'
|
|
846
|
-
}}
|
|
847
|
-
/>
|
|
848
|
-
|
|
849
|
-
{/* Mobile Path dropdown */}
|
|
850
|
-
{showPathDropdown && filteredPaths.length > 0 && (
|
|
851
|
-
<div className="absolute bottom-full left-0 right-0 mb-2 bg-popover border border-border rounded-md shadow-lg max-h-40 overflow-y-auto">
|
|
852
|
-
{filteredPaths.map((pathItem, index) => (
|
|
853
|
-
<div
|
|
854
|
-
key={pathItem.path}
|
|
855
|
-
className={`px-3 py-2.5 cursor-pointer border-b border-border last:border-b-0 active:scale-95 transition-all ${
|
|
856
|
-
index === selectedPathIndex
|
|
857
|
-
? 'bg-accent text-accent-foreground'
|
|
858
|
-
: 'hover:bg-accent/50'
|
|
859
|
-
}`}
|
|
860
|
-
onMouseDown={(e) => {
|
|
861
|
-
e.preventDefault();
|
|
862
|
-
e.stopPropagation();
|
|
863
|
-
}}
|
|
864
|
-
onClick={(e) => {
|
|
865
|
-
e.preventDefault();
|
|
866
|
-
e.stopPropagation();
|
|
867
|
-
selectPath(pathItem);
|
|
868
|
-
}}
|
|
869
|
-
>
|
|
870
|
-
<div className="flex items-center gap-2">
|
|
871
|
-
<Folder className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
|
872
|
-
<div>
|
|
873
|
-
<div className="font-medium text-sm">{pathItem.name}</div>
|
|
874
|
-
<div className="text-xs text-muted-foreground font-mono">
|
|
875
|
-
{pathItem.path}
|
|
876
|
-
</div>
|
|
877
|
-
</div>
|
|
878
|
-
</div>
|
|
879
|
-
</div>
|
|
880
|
-
))}
|
|
881
|
-
</div>
|
|
882
|
-
)}
|
|
883
|
-
</div>
|
|
884
|
-
|
|
885
|
-
<div className="flex gap-2">
|
|
886
|
-
<Button
|
|
887
|
-
onClick={cancelNewProject}
|
|
888
|
-
disabled={creatingProject}
|
|
889
|
-
variant="outline"
|
|
890
|
-
className="flex-1 h-9 text-sm rounded-md active:scale-95 transition-transform"
|
|
891
|
-
>
|
|
892
|
-
Cancel
|
|
893
|
-
</Button>
|
|
894
|
-
<Button
|
|
895
|
-
onClick={createNewProject}
|
|
896
|
-
disabled={!newProjectPath.trim() || creatingProject}
|
|
897
|
-
className="flex-1 h-9 text-sm rounded-md bg-primary hover:bg-primary/90 active:scale-95 transition-all"
|
|
898
|
-
>
|
|
899
|
-
{creatingProject ? 'Creating...' : 'Create'}
|
|
900
|
-
</Button>
|
|
901
|
-
</div>
|
|
902
|
-
</div>
|
|
903
|
-
|
|
904
|
-
{/* Safe area for mobile */}
|
|
905
|
-
<div className="h-4" />
|
|
906
|
-
</div>
|
|
907
|
-
</div>
|
|
908
|
-
</div>
|
|
909
|
-
)}
|
|
910
|
-
|
|
911
|
-
{/* Search Filter */}
|
|
912
|
-
{projects.length > 0 && !isLoading && (
|
|
913
|
-
<div className="px-3 md:px-4 py-2 border-b border-border">
|
|
914
|
-
<div className="relative">
|
|
915
|
-
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
916
|
-
<Input
|
|
917
|
-
type="text"
|
|
918
|
-
placeholder="Search projects..."
|
|
919
|
-
value={searchFilter}
|
|
920
|
-
onChange={(e) => setSearchFilter(e.target.value)}
|
|
921
|
-
className="pl-9 h-9 text-sm bg-muted/50 border-0 focus:bg-background focus:ring-1 focus:ring-primary/20"
|
|
922
|
-
/>
|
|
923
|
-
{searchFilter && (
|
|
924
|
-
<button
|
|
925
|
-
onClick={() => setSearchFilter('')}
|
|
926
|
-
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-1 hover:bg-accent rounded"
|
|
927
|
-
>
|
|
928
|
-
<X className="w-3 h-3 text-muted-foreground" />
|
|
929
|
-
</button>
|
|
930
|
-
)}
|
|
931
|
-
</div>
|
|
932
|
-
</div>
|
|
933
|
-
)}
|
|
934
|
-
|
|
935
|
-
{/* Projects List */}
|
|
936
|
-
<ScrollArea className="flex-1 md:px-2 md:py-3 overflow-y-auto overscroll-contain">
|
|
937
|
-
<div className="md:space-y-1 pb-safe-area-inset-bottom">
|
|
938
|
-
{isLoading ? (
|
|
939
|
-
<div className="text-center py-12 md:py-8 px-4">
|
|
940
|
-
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
|
941
|
-
<div className="w-6 h-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
|
942
|
-
</div>
|
|
943
|
-
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">Loading projects...</h3>
|
|
944
|
-
<p className="text-sm text-muted-foreground">
|
|
945
|
-
Fetching your Claude projects and sessions
|
|
946
|
-
</p>
|
|
947
|
-
</div>
|
|
948
|
-
) : projects.length === 0 ? (
|
|
949
|
-
<div className="text-center py-12 md:py-8 px-4">
|
|
950
|
-
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
|
951
|
-
<Folder className="w-6 h-6 text-muted-foreground" />
|
|
952
|
-
</div>
|
|
953
|
-
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">No projects found</h3>
|
|
954
|
-
<p className="text-sm text-muted-foreground">
|
|
955
|
-
Run Claude CLI in a project directory to get started
|
|
956
|
-
</p>
|
|
957
|
-
</div>
|
|
958
|
-
) : filteredProjects.length === 0 ? (
|
|
959
|
-
<div className="text-center py-12 md:py-8 px-4">
|
|
960
|
-
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
|
961
|
-
<Search className="w-6 h-6 text-muted-foreground" />
|
|
962
|
-
</div>
|
|
963
|
-
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">No matching projects</h3>
|
|
964
|
-
<p className="text-sm text-muted-foreground">
|
|
965
|
-
Try adjusting your search term
|
|
966
|
-
</p>
|
|
967
|
-
</div>
|
|
968
|
-
) : (
|
|
969
|
-
filteredProjects.map((project) => {
|
|
970
|
-
const isExpanded = expandedProjects.has(project.name);
|
|
971
|
-
const isSelected = selectedProject?.name === project.name;
|
|
972
|
-
const isStarred = isProjectStarred(project.name);
|
|
973
|
-
|
|
974
|
-
return (
|
|
975
|
-
<div key={project.name} className="md:space-y-1">
|
|
976
|
-
{/* Project Header */}
|
|
977
|
-
<div className="group md:group">
|
|
978
|
-
{/* Mobile Project Item */}
|
|
979
|
-
<div className="md:hidden">
|
|
980
|
-
<div
|
|
981
|
-
className={cn(
|
|
982
|
-
"p-3 mx-3 my-1 rounded-lg bg-card border border-border/50 active:scale-[0.98] transition-all duration-150",
|
|
983
|
-
isSelected && "bg-primary/5 border-primary/20",
|
|
984
|
-
isStarred && !isSelected && "bg-yellow-50/50 dark:bg-yellow-900/5 border-yellow-200/30 dark:border-yellow-800/30"
|
|
985
|
-
)}
|
|
986
|
-
onClick={() => {
|
|
987
|
-
// On mobile, just toggle the folder - don't select the project
|
|
988
|
-
toggleProject(project.name);
|
|
989
|
-
}}
|
|
990
|
-
onTouchEnd={handleTouchClick(() => toggleProject(project.name))}
|
|
991
|
-
>
|
|
992
|
-
<div className="flex items-center justify-between">
|
|
993
|
-
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
994
|
-
<div className={cn(
|
|
995
|
-
"w-8 h-8 rounded-lg flex items-center justify-center transition-colors",
|
|
996
|
-
isExpanded ? "bg-primary/10" : "bg-muted"
|
|
997
|
-
)}>
|
|
998
|
-
{isExpanded ? (
|
|
999
|
-
<FolderOpen className="w-4 h-4 text-primary" />
|
|
1000
|
-
) : (
|
|
1001
|
-
<Folder className="w-4 h-4 text-muted-foreground" />
|
|
1002
|
-
)}
|
|
1003
|
-
</div>
|
|
1004
|
-
<div className="min-w-0 flex-1">
|
|
1005
|
-
{editingProject === project.name ? (
|
|
1006
|
-
<input
|
|
1007
|
-
type="text"
|
|
1008
|
-
value={editingName}
|
|
1009
|
-
onChange={(e) => setEditingName(e.target.value)}
|
|
1010
|
-
className="w-full px-3 py-2 text-sm border-2 border-primary/40 focus:border-primary rounded-lg bg-background text-foreground shadow-sm focus:shadow-md transition-all duration-200 focus:outline-none"
|
|
1011
|
-
placeholder="Project name"
|
|
1012
|
-
autoFocus
|
|
1013
|
-
autoComplete="off"
|
|
1014
|
-
onClick={(e) => e.stopPropagation()}
|
|
1015
|
-
onKeyDown={(e) => {
|
|
1016
|
-
if (e.key === 'Enter') saveProjectName(project.name);
|
|
1017
|
-
if (e.key === 'Escape') cancelEditing();
|
|
1018
|
-
}}
|
|
1019
|
-
style={{
|
|
1020
|
-
fontSize: '16px', // Prevents zoom on iOS
|
|
1021
|
-
WebkitAppearance: 'none',
|
|
1022
|
-
borderRadius: '8px'
|
|
1023
|
-
}}
|
|
1024
|
-
/>
|
|
1025
|
-
) : (
|
|
1026
|
-
<>
|
|
1027
|
-
<div className="flex items-center justify-between min-w-0 flex-1">
|
|
1028
|
-
<h3 className="text-sm font-medium text-foreground truncate">
|
|
1029
|
-
{project.displayName}
|
|
1030
|
-
</h3>
|
|
1031
|
-
{tasksEnabled && (
|
|
1032
|
-
<TaskIndicator
|
|
1033
|
-
status={(() => {
|
|
1034
|
-
const projectConfigured = project.taskmaster?.hasTaskmaster;
|
|
1035
|
-
const mcpConfigured = mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured;
|
|
1036
|
-
if (projectConfigured && mcpConfigured) return 'fully-configured';
|
|
1037
|
-
if (projectConfigured) return 'taskmaster-only';
|
|
1038
|
-
if (mcpConfigured) return 'mcp-only';
|
|
1039
|
-
return 'not-configured';
|
|
1040
|
-
})()}
|
|
1041
|
-
size="xs"
|
|
1042
|
-
className="flex-shrink-0 ml-2"
|
|
1043
|
-
/>
|
|
1044
|
-
)}
|
|
1045
|
-
</div>
|
|
1046
|
-
<p className="text-xs text-muted-foreground">
|
|
1047
|
-
{(() => {
|
|
1048
|
-
const sessionCount = getAllSessions(project).length;
|
|
1049
|
-
const hasMore = project.sessionMeta?.hasMore !== false;
|
|
1050
|
-
const count = hasMore && sessionCount >= 5 ? `${sessionCount}+` : sessionCount;
|
|
1051
|
-
return `${count} session${count === 1 ? '' : 's'}`;
|
|
1052
|
-
})()}
|
|
1053
|
-
</p>
|
|
1054
|
-
</>
|
|
1055
|
-
)}
|
|
1056
|
-
</div>
|
|
1057
|
-
</div>
|
|
1058
|
-
<div className="flex items-center gap-1">
|
|
1059
|
-
{editingProject === project.name ? (
|
|
1060
|
-
<>
|
|
1061
|
-
<button
|
|
1062
|
-
className="w-8 h-8 rounded-lg bg-green-500 dark:bg-green-600 flex items-center justify-center active:scale-90 transition-all duration-150 shadow-sm active:shadow-none"
|
|
1063
|
-
onClick={(e) => {
|
|
1064
|
-
e.stopPropagation();
|
|
1065
|
-
saveProjectName(project.name);
|
|
1066
|
-
}}
|
|
1067
|
-
>
|
|
1068
|
-
<Check className="w-4 h-4 text-white" />
|
|
1069
|
-
</button>
|
|
1070
|
-
<button
|
|
1071
|
-
className="w-8 h-8 rounded-lg bg-gray-500 dark:bg-gray-600 flex items-center justify-center active:scale-90 transition-all duration-150 shadow-sm active:shadow-none"
|
|
1072
|
-
onClick={(e) => {
|
|
1073
|
-
e.stopPropagation();
|
|
1074
|
-
cancelEditing();
|
|
1075
|
-
}}
|
|
1076
|
-
>
|
|
1077
|
-
<X className="w-4 h-4 text-white" />
|
|
1078
|
-
</button>
|
|
1079
|
-
</>
|
|
1080
|
-
) : (
|
|
1081
|
-
<>
|
|
1082
|
-
{/* Star button */}
|
|
1083
|
-
<button
|
|
1084
|
-
className={cn(
|
|
1085
|
-
"w-8 h-8 rounded-lg flex items-center justify-center active:scale-90 transition-all duration-150 border",
|
|
1086
|
-
isStarred
|
|
1087
|
-
? "bg-yellow-500/10 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800"
|
|
1088
|
-
: "bg-gray-500/10 dark:bg-gray-900/30 border-gray-200 dark:border-gray-800"
|
|
1089
|
-
)}
|
|
1090
|
-
onClick={(e) => {
|
|
1091
|
-
e.stopPropagation();
|
|
1092
|
-
toggleStarProject(project.name);
|
|
1093
|
-
}}
|
|
1094
|
-
onTouchEnd={handleTouchClick(() => toggleStarProject(project.name))}
|
|
1095
|
-
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
|
1096
|
-
>
|
|
1097
|
-
<Star className={cn(
|
|
1098
|
-
"w-4 h-4 transition-colors",
|
|
1099
|
-
isStarred
|
|
1100
|
-
? "text-yellow-600 dark:text-yellow-400 fill-current"
|
|
1101
|
-
: "text-gray-600 dark:text-gray-400"
|
|
1102
|
-
)} />
|
|
1103
|
-
</button>
|
|
1104
|
-
{getAllSessions(project).length === 0 && (
|
|
1105
|
-
<button
|
|
1106
|
-
className="w-8 h-8 rounded-lg bg-red-500/10 dark:bg-red-900/30 flex items-center justify-center active:scale-90 border border-red-200 dark:border-red-800"
|
|
1107
|
-
onClick={(e) => {
|
|
1108
|
-
e.stopPropagation();
|
|
1109
|
-
deleteProject(project.name);
|
|
1110
|
-
}}
|
|
1111
|
-
onTouchEnd={handleTouchClick(() => deleteProject(project.name))}
|
|
1112
|
-
>
|
|
1113
|
-
<Trash2 className="w-4 h-4 text-red-600 dark:text-red-400" />
|
|
1114
|
-
</button>
|
|
1115
|
-
)}
|
|
1116
|
-
<button
|
|
1117
|
-
className="w-8 h-8 rounded-lg bg-primary/10 dark:bg-primary/20 flex items-center justify-center active:scale-90 border border-primary/20 dark:border-primary/30"
|
|
1118
|
-
onClick={(e) => {
|
|
1119
|
-
e.stopPropagation();
|
|
1120
|
-
startEditing(project);
|
|
1121
|
-
}}
|
|
1122
|
-
onTouchEnd={handleTouchClick(() => startEditing(project))}
|
|
1123
|
-
>
|
|
1124
|
-
<Edit3 className="w-4 h-4 text-primary" />
|
|
1125
|
-
</button>
|
|
1126
|
-
<div className="w-6 h-6 rounded-md bg-muted/30 flex items-center justify-center">
|
|
1127
|
-
{isExpanded ? (
|
|
1128
|
-
<ChevronDown className="w-3 h-3 text-muted-foreground" />
|
|
1129
|
-
) : (
|
|
1130
|
-
<ChevronRight className="w-3 h-3 text-muted-foreground" />
|
|
1131
|
-
)}
|
|
1132
|
-
</div>
|
|
1133
|
-
</>
|
|
1134
|
-
)}
|
|
1135
|
-
</div>
|
|
1136
|
-
</div>
|
|
1137
|
-
</div>
|
|
1138
|
-
</div>
|
|
1139
|
-
|
|
1140
|
-
{/* Desktop Project Item */}
|
|
1141
|
-
<Button
|
|
1142
|
-
variant="ghost"
|
|
1143
|
-
className={cn(
|
|
1144
|
-
"hidden md:flex w-full justify-between p-2 h-auto font-normal hover:bg-accent/50",
|
|
1145
|
-
isSelected && "bg-accent text-accent-foreground",
|
|
1146
|
-
isStarred && !isSelected && "bg-yellow-50/50 dark:bg-yellow-900/10 hover:bg-yellow-100/50 dark:hover:bg-yellow-900/20"
|
|
1147
|
-
)}
|
|
1148
|
-
onClick={() => {
|
|
1149
|
-
// Desktop behavior: select project and toggle
|
|
1150
|
-
if (selectedProject?.name !== project.name) {
|
|
1151
|
-
handleProjectSelect(project);
|
|
1152
|
-
}
|
|
1153
|
-
toggleProject(project.name);
|
|
1154
|
-
}}
|
|
1155
|
-
onTouchEnd={handleTouchClick(() => {
|
|
1156
|
-
if (selectedProject?.name !== project.name) {
|
|
1157
|
-
handleProjectSelect(project);
|
|
1158
|
-
}
|
|
1159
|
-
toggleProject(project.name);
|
|
1160
|
-
})}
|
|
1161
|
-
>
|
|
1162
|
-
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
1163
|
-
{isExpanded ? (
|
|
1164
|
-
<FolderOpen className="w-4 h-4 text-primary flex-shrink-0" />
|
|
1165
|
-
) : (
|
|
1166
|
-
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
1167
|
-
)}
|
|
1168
|
-
<div className="min-w-0 flex-1 text-left">
|
|
1169
|
-
{editingProject === project.name ? (
|
|
1170
|
-
<div className="space-y-1">
|
|
1171
|
-
<input
|
|
1172
|
-
type="text"
|
|
1173
|
-
value={editingName}
|
|
1174
|
-
onChange={(e) => setEditingName(e.target.value)}
|
|
1175
|
-
className="w-full px-2 py-1 text-sm border border-border rounded bg-background text-foreground focus:ring-2 focus:ring-primary/20"
|
|
1176
|
-
placeholder="Project name"
|
|
1177
|
-
autoFocus
|
|
1178
|
-
onKeyDown={(e) => {
|
|
1179
|
-
if (e.key === 'Enter') saveProjectName(project.name);
|
|
1180
|
-
if (e.key === 'Escape') cancelEditing();
|
|
1181
|
-
}}
|
|
1182
|
-
/>
|
|
1183
|
-
<div className="text-xs text-muted-foreground truncate" title={project.fullPath}>
|
|
1184
|
-
{project.fullPath}
|
|
1185
|
-
</div>
|
|
1186
|
-
</div>
|
|
1187
|
-
) : (
|
|
1188
|
-
<div>
|
|
1189
|
-
<div className="text-sm font-semibold truncate text-foreground" title={project.displayName}>
|
|
1190
|
-
{project.displayName}
|
|
1191
|
-
</div>
|
|
1192
|
-
<div className="text-xs text-muted-foreground">
|
|
1193
|
-
{(() => {
|
|
1194
|
-
const sessionCount = getAllSessions(project).length;
|
|
1195
|
-
const hasMore = project.sessionMeta?.hasMore !== false;
|
|
1196
|
-
return hasMore && sessionCount >= 5 ? `${sessionCount}+` : sessionCount;
|
|
1197
|
-
})()}
|
|
1198
|
-
{project.fullPath !== project.displayName && (
|
|
1199
|
-
<span className="ml-1 opacity-60" title={project.fullPath}>
|
|
1200
|
-
• {project.fullPath.length > 25 ? '...' + project.fullPath.slice(-22) : project.fullPath}
|
|
1201
|
-
</span>
|
|
1202
|
-
)}
|
|
1203
|
-
</div>
|
|
1204
|
-
</div>
|
|
1205
|
-
)}
|
|
1206
|
-
</div>
|
|
1207
|
-
</div>
|
|
1208
|
-
|
|
1209
|
-
<div className="flex items-center gap-1 flex-shrink-0">
|
|
1210
|
-
{editingProject === project.name ? (
|
|
1211
|
-
<>
|
|
1212
|
-
<div
|
|
1213
|
-
className="w-6 h-6 text-green-600 hover:text-green-700 hover:bg-green-50 dark:hover:bg-green-900/20 flex items-center justify-center rounded cursor-pointer transition-colors"
|
|
1214
|
-
onClick={(e) => {
|
|
1215
|
-
e.stopPropagation();
|
|
1216
|
-
saveProjectName(project.name);
|
|
1217
|
-
}}
|
|
1218
|
-
>
|
|
1219
|
-
<Check className="w-3 h-3" />
|
|
1220
|
-
</div>
|
|
1221
|
-
<div
|
|
1222
|
-
className="w-6 h-6 text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center justify-center rounded cursor-pointer transition-colors"
|
|
1223
|
-
onClick={(e) => {
|
|
1224
|
-
e.stopPropagation();
|
|
1225
|
-
cancelEditing();
|
|
1226
|
-
}}
|
|
1227
|
-
>
|
|
1228
|
-
<X className="w-3 h-3" />
|
|
1229
|
-
</div>
|
|
1230
|
-
</>
|
|
1231
|
-
) : (
|
|
1232
|
-
<>
|
|
1233
|
-
{/* Star button */}
|
|
1234
|
-
<div
|
|
1235
|
-
className={cn(
|
|
1236
|
-
"w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center rounded cursor-pointer touch:opacity-100",
|
|
1237
|
-
isStarred
|
|
1238
|
-
? "hover:bg-yellow-50 dark:hover:bg-yellow-900/20 opacity-100"
|
|
1239
|
-
: "hover:bg-accent"
|
|
1240
|
-
)}
|
|
1241
|
-
onClick={(e) => {
|
|
1242
|
-
e.stopPropagation();
|
|
1243
|
-
toggleStarProject(project.name);
|
|
1244
|
-
}}
|
|
1245
|
-
title={isStarred ? "Remove from favorites" : "Add to favorites"}
|
|
1246
|
-
>
|
|
1247
|
-
<Star className={cn(
|
|
1248
|
-
"w-3 h-3 transition-colors",
|
|
1249
|
-
isStarred
|
|
1250
|
-
? "text-yellow-600 dark:text-yellow-400 fill-current"
|
|
1251
|
-
: "text-muted-foreground"
|
|
1252
|
-
)} />
|
|
1253
|
-
</div>
|
|
1254
|
-
<div
|
|
1255
|
-
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-accent flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
|
1256
|
-
onClick={(e) => {
|
|
1257
|
-
e.stopPropagation();
|
|
1258
|
-
startEditing(project);
|
|
1259
|
-
}}
|
|
1260
|
-
title="Rename project (F2)"
|
|
1261
|
-
>
|
|
1262
|
-
<Edit3 className="w-3 h-3" />
|
|
1263
|
-
</div>
|
|
1264
|
-
{getAllSessions(project).length === 0 && (
|
|
1265
|
-
<div
|
|
1266
|
-
className="w-6 h-6 opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center justify-center rounded cursor-pointer touch:opacity-100"
|
|
1267
|
-
onClick={(e) => {
|
|
1268
|
-
e.stopPropagation();
|
|
1269
|
-
deleteProject(project.name);
|
|
1270
|
-
}}
|
|
1271
|
-
title="Delete empty project (Delete)"
|
|
1272
|
-
>
|
|
1273
|
-
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
|
1274
|
-
</div>
|
|
1275
|
-
)}
|
|
1276
|
-
{isExpanded ? (
|
|
1277
|
-
<ChevronDown className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
1278
|
-
) : (
|
|
1279
|
-
<ChevronRight className="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
|
1280
|
-
)}
|
|
1281
|
-
</>
|
|
1282
|
-
)}
|
|
1283
|
-
</div>
|
|
1284
|
-
</Button>
|
|
1285
|
-
</div>
|
|
1286
|
-
|
|
1287
|
-
{/* Sessions List */}
|
|
1288
|
-
{isExpanded && (
|
|
1289
|
-
<div className="ml-3 space-y-1 border-l border-border pl-3">
|
|
1290
|
-
{!initialSessionsLoaded.has(project.name) ? (
|
|
1291
|
-
// Loading skeleton for sessions
|
|
1292
|
-
Array.from({ length: 3 }).map((_, i) => (
|
|
1293
|
-
<div key={i} className="p-2 rounded-md">
|
|
1294
|
-
<div className="flex items-start gap-2">
|
|
1295
|
-
<div className="w-3 h-3 bg-muted rounded-full animate-pulse mt-0.5" />
|
|
1296
|
-
<div className="flex-1 space-y-1">
|
|
1297
|
-
<div className="h-3 bg-muted rounded animate-pulse" style={{ width: `${60 + i * 15}%` }} />
|
|
1298
|
-
<div className="h-2 bg-muted rounded animate-pulse w-1/2" />
|
|
1299
|
-
</div>
|
|
1300
|
-
</div>
|
|
1301
|
-
</div>
|
|
1302
|
-
))
|
|
1303
|
-
) : getAllSessions(project).length === 0 && !loadingSessions[project.name] ? (
|
|
1304
|
-
<div className="py-2 px-3 text-left">
|
|
1305
|
-
<p className="text-xs text-muted-foreground">No sessions yet</p>
|
|
1306
|
-
</div>
|
|
1307
|
-
) : (
|
|
1308
|
-
getAllSessions(project).map((session) => {
|
|
1309
|
-
// Handle both Claude and Cursor session formats
|
|
1310
|
-
const isCursorSession = session.__provider === 'cursor';
|
|
1311
|
-
|
|
1312
|
-
// Calculate if session is active (within last 10 minutes)
|
|
1313
|
-
const sessionDate = new Date(isCursorSession ? session.createdAt : session.lastActivity);
|
|
1314
|
-
const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60));
|
|
1315
|
-
const isActive = diffInMinutes < 10;
|
|
1316
|
-
|
|
1317
|
-
// Get session display values
|
|
1318
|
-
const sessionName = isCursorSession ? (session.name || 'Untitled Session') : (session.summary || 'New Session');
|
|
1319
|
-
const sessionTime = isCursorSession ? session.createdAt : session.lastActivity;
|
|
1320
|
-
const messageCount = session.messageCount || 0;
|
|
1321
|
-
|
|
1322
|
-
return (
|
|
1323
|
-
<div key={session.id} className="group relative">
|
|
1324
|
-
{/* Active session indicator dot */}
|
|
1325
|
-
{isActive && (
|
|
1326
|
-
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 -translate-x-1">
|
|
1327
|
-
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
1328
|
-
</div>
|
|
1329
|
-
)}
|
|
1330
|
-
{/* Mobile Session Item */}
|
|
1331
|
-
<div className="md:hidden">
|
|
1332
|
-
<div
|
|
1333
|
-
className={cn(
|
|
1334
|
-
"p-2 mx-3 my-0.5 rounded-md bg-card border active:scale-[0.98] transition-all duration-150 relative",
|
|
1335
|
-
selectedSession?.id === session.id ? "bg-primary/5 border-primary/20" :
|
|
1336
|
-
isActive ? "border-green-500/30 bg-green-50/5 dark:bg-green-900/5" : "border-border/30"
|
|
1337
|
-
)}
|
|
1338
|
-
onClick={() => {
|
|
1339
|
-
handleProjectSelect(project);
|
|
1340
|
-
onSessionSelect(session);
|
|
1341
|
-
}}
|
|
1342
|
-
onTouchEnd={handleTouchClick(() => {
|
|
1343
|
-
handleProjectSelect(project);
|
|
1344
|
-
onSessionSelect(session);
|
|
1345
|
-
})}
|
|
1346
|
-
>
|
|
1347
|
-
<div className="flex items-center gap-2">
|
|
1348
|
-
<div className={cn(
|
|
1349
|
-
"w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0",
|
|
1350
|
-
selectedSession?.id === session.id ? "bg-primary/10" : "bg-muted/50"
|
|
1351
|
-
)}>
|
|
1352
|
-
{isCursorSession ? (
|
|
1353
|
-
<CursorLogo className="w-3 h-3" />
|
|
1354
|
-
) : (
|
|
1355
|
-
<ClaudeLogo className="w-3 h-3" />
|
|
1356
|
-
)}
|
|
1357
|
-
</div>
|
|
1358
|
-
<div className="min-w-0 flex-1">
|
|
1359
|
-
<div className="text-xs font-medium truncate text-foreground">
|
|
1360
|
-
{sessionName}
|
|
1361
|
-
</div>
|
|
1362
|
-
<div className="flex items-center gap-1 mt-0.5">
|
|
1363
|
-
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
|
|
1364
|
-
<span className="text-xs text-muted-foreground">
|
|
1365
|
-
{formatTimeAgo(sessionTime, currentTime)}
|
|
1366
|
-
</span>
|
|
1367
|
-
{messageCount > 0 && (
|
|
1368
|
-
<Badge variant="secondary" className="text-xs px-1 py-0 ml-auto">
|
|
1369
|
-
{messageCount}
|
|
1370
|
-
</Badge>
|
|
1371
|
-
)}
|
|
1372
|
-
{/* Provider tiny icon */}
|
|
1373
|
-
<span className="ml-1 opacity-70">
|
|
1374
|
-
{isCursorSession ? (
|
|
1375
|
-
<CursorLogo className="w-3 h-3" />
|
|
1376
|
-
) : (
|
|
1377
|
-
<ClaudeLogo className="w-3 h-3" />
|
|
1378
|
-
)}
|
|
1379
|
-
</span>
|
|
1380
|
-
</div>
|
|
1381
|
-
</div>
|
|
1382
|
-
{/* Mobile delete button - only for Claude sessions */}
|
|
1383
|
-
{!isCursorSession && (
|
|
1384
|
-
<button
|
|
1385
|
-
className="w-5 h-5 rounded-md bg-red-50 dark:bg-red-900/20 flex items-center justify-center active:scale-95 transition-transform opacity-70 ml-1"
|
|
1386
|
-
onClick={(e) => {
|
|
1387
|
-
e.stopPropagation();
|
|
1388
|
-
deleteSession(project.name, session.id);
|
|
1389
|
-
}}
|
|
1390
|
-
onTouchEnd={handleTouchClick(() => deleteSession(project.name, session.id))}
|
|
1391
|
-
>
|
|
1392
|
-
<Trash2 className="w-2.5 h-2.5 text-red-600 dark:text-red-400" />
|
|
1393
|
-
</button>
|
|
1394
|
-
)}
|
|
1395
|
-
</div>
|
|
1396
|
-
</div>
|
|
1397
|
-
</div>
|
|
1398
|
-
|
|
1399
|
-
{/* Desktop Session Item */}
|
|
1400
|
-
<div className="hidden md:block">
|
|
1401
|
-
<Button
|
|
1402
|
-
variant="ghost"
|
|
1403
|
-
className={cn(
|
|
1404
|
-
"w-full justify-start p-2 h-auto font-normal text-left hover:bg-accent/50 transition-colors duration-200",
|
|
1405
|
-
selectedSession?.id === session.id && "bg-accent text-accent-foreground"
|
|
1406
|
-
)}
|
|
1407
|
-
onClick={() => onSessionSelect(session)}
|
|
1408
|
-
onTouchEnd={handleTouchClick(() => onSessionSelect(session))}
|
|
1409
|
-
>
|
|
1410
|
-
<div className="flex items-start gap-2 min-w-0 w-full">
|
|
1411
|
-
{isCursorSession ? (
|
|
1412
|
-
<CursorLogo className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
|
1413
|
-
) : (
|
|
1414
|
-
<ClaudeLogo className="w-3 h-3 mt-0.5 flex-shrink-0" />
|
|
1415
|
-
)}
|
|
1416
|
-
<div className="min-w-0 flex-1">
|
|
1417
|
-
<div className="text-xs font-medium truncate text-foreground">
|
|
1418
|
-
{sessionName}
|
|
1419
|
-
</div>
|
|
1420
|
-
<div className="flex items-center gap-1 mt-0.5">
|
|
1421
|
-
<Clock className="w-2.5 h-2.5 text-muted-foreground" />
|
|
1422
|
-
<span className="text-xs text-muted-foreground">
|
|
1423
|
-
{formatTimeAgo(sessionTime, currentTime)}
|
|
1424
|
-
</span>
|
|
1425
|
-
{messageCount > 0 && (
|
|
1426
|
-
<Badge variant="secondary" className="text-xs px-1 py-0 ml-auto">
|
|
1427
|
-
{messageCount}
|
|
1428
|
-
</Badge>
|
|
1429
|
-
)}
|
|
1430
|
-
{/* Provider tiny icon */}
|
|
1431
|
-
<span className="ml-1 opacity-70">
|
|
1432
|
-
{isCursorSession ? (
|
|
1433
|
-
<CursorLogo className="w-3 h-3" />
|
|
1434
|
-
) : (
|
|
1435
|
-
<ClaudeLogo className="w-3 h-3" />
|
|
1436
|
-
)}
|
|
1437
|
-
</span>
|
|
1438
|
-
</div>
|
|
1439
|
-
</div>
|
|
1440
|
-
</div>
|
|
1441
|
-
</Button>
|
|
1442
|
-
{/* Desktop hover buttons - only for Claude sessions */}
|
|
1443
|
-
{!isCursorSession && (
|
|
1444
|
-
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-200">
|
|
1445
|
-
{editingSession === session.id ? (
|
|
1446
|
-
<>
|
|
1447
|
-
<input
|
|
1448
|
-
type="text"
|
|
1449
|
-
value={editingSessionName}
|
|
1450
|
-
onChange={(e) => setEditingSessionName(e.target.value)}
|
|
1451
|
-
onKeyDown={(e) => {
|
|
1452
|
-
e.stopPropagation();
|
|
1453
|
-
if (e.key === 'Enter') {
|
|
1454
|
-
updateSessionSummary(project.name, session.id, editingSessionName);
|
|
1455
|
-
} else if (e.key === 'Escape') {
|
|
1456
|
-
setEditingSession(null);
|
|
1457
|
-
setEditingSessionName('');
|
|
1458
|
-
}
|
|
1459
|
-
}}
|
|
1460
|
-
onClick={(e) => e.stopPropagation()}
|
|
1461
|
-
className="w-32 px-2 py-1 text-xs border border-border rounded bg-background focus:outline-none focus:ring-1 focus:ring-primary"
|
|
1462
|
-
autoFocus
|
|
1463
|
-
/>
|
|
1464
|
-
<button
|
|
1465
|
-
className="w-6 h-6 bg-green-50 hover:bg-green-100 dark:bg-green-900/20 dark:hover:bg-green-900/40 rounded flex items-center justify-center"
|
|
1466
|
-
onClick={(e) => {
|
|
1467
|
-
e.stopPropagation();
|
|
1468
|
-
updateSessionSummary(project.name, session.id, editingSessionName);
|
|
1469
|
-
}}
|
|
1470
|
-
title="Save"
|
|
1471
|
-
>
|
|
1472
|
-
<Check className="w-3 h-3 text-green-600 dark:text-green-400" />
|
|
1473
|
-
</button>
|
|
1474
|
-
<button
|
|
1475
|
-
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
|
|
1476
|
-
onClick={(e) => {
|
|
1477
|
-
e.stopPropagation();
|
|
1478
|
-
setEditingSession(null);
|
|
1479
|
-
setEditingSessionName('');
|
|
1480
|
-
}}
|
|
1481
|
-
title="Cancel"
|
|
1482
|
-
>
|
|
1483
|
-
<X className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
|
1484
|
-
</button>
|
|
1485
|
-
</>
|
|
1486
|
-
) : (
|
|
1487
|
-
<>
|
|
1488
|
-
{/* Generate summary button */}
|
|
1489
|
-
{/* <button
|
|
1490
|
-
className="w-6 h-6 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:hover:bg-blue-900/40 rounded flex items-center justify-center"
|
|
1491
|
-
onClick={(e) => {
|
|
1492
|
-
e.stopPropagation();
|
|
1493
|
-
generateSessionSummary(project.name, session.id);
|
|
1494
|
-
}}
|
|
1495
|
-
title="Generate AI summary for this session"
|
|
1496
|
-
disabled={generatingSummary[`${project.name}-${session.id}`]}
|
|
1497
|
-
>
|
|
1498
|
-
{generatingSummary[`${project.name}-${session.id}`] ? (
|
|
1499
|
-
<div className="w-3 h-3 animate-spin rounded-full border border-blue-600 dark:border-blue-400 border-t-transparent" />
|
|
1500
|
-
) : (
|
|
1501
|
-
<Sparkles className="w-3 h-3 text-blue-600 dark:text-blue-400" />
|
|
1502
|
-
)}
|
|
1503
|
-
</button> */}
|
|
1504
|
-
{/* Edit button */}
|
|
1505
|
-
<button
|
|
1506
|
-
className="w-6 h-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900/20 dark:hover:bg-gray-900/40 rounded flex items-center justify-center"
|
|
1507
|
-
onClick={(e) => {
|
|
1508
|
-
e.stopPropagation();
|
|
1509
|
-
setEditingSession(session.id);
|
|
1510
|
-
setEditingSessionName(session.summary || 'New Session');
|
|
1511
|
-
}}
|
|
1512
|
-
title="Manually edit session name"
|
|
1513
|
-
>
|
|
1514
|
-
<Edit2 className="w-3 h-3 text-gray-600 dark:text-gray-400" />
|
|
1515
|
-
</button>
|
|
1516
|
-
{/* Delete button */}
|
|
1517
|
-
<button
|
|
1518
|
-
className="w-6 h-6 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 rounded flex items-center justify-center"
|
|
1519
|
-
onClick={(e) => {
|
|
1520
|
-
e.stopPropagation();
|
|
1521
|
-
deleteSession(project.name, session.id);
|
|
1522
|
-
}}
|
|
1523
|
-
title="Delete this session permanently"
|
|
1524
|
-
>
|
|
1525
|
-
<Trash2 className="w-3 h-3 text-red-600 dark:text-red-400" />
|
|
1526
|
-
</button>
|
|
1527
|
-
</>
|
|
1528
|
-
)}
|
|
1529
|
-
</div>
|
|
1530
|
-
)}
|
|
1531
|
-
</div>
|
|
1532
|
-
</div>
|
|
1533
|
-
);
|
|
1534
|
-
})
|
|
1535
|
-
)}
|
|
1536
|
-
|
|
1537
|
-
{/* Show More Sessions Button */}
|
|
1538
|
-
{getAllSessions(project).length > 0 && project.sessionMeta?.hasMore !== false && (
|
|
1539
|
-
<Button
|
|
1540
|
-
variant="ghost"
|
|
1541
|
-
size="sm"
|
|
1542
|
-
className="w-full justify-center gap-2 mt-2 text-muted-foreground"
|
|
1543
|
-
onClick={() => loadMoreSessions(project)}
|
|
1544
|
-
disabled={loadingSessions[project.name]}
|
|
1545
|
-
>
|
|
1546
|
-
{loadingSessions[project.name] ? (
|
|
1547
|
-
<>
|
|
1548
|
-
<div className="w-3 h-3 animate-spin rounded-full border border-muted-foreground border-t-transparent" />
|
|
1549
|
-
Loading...
|
|
1550
|
-
</>
|
|
1551
|
-
) : (
|
|
1552
|
-
<>
|
|
1553
|
-
<ChevronDown className="w-3 h-3" />
|
|
1554
|
-
Show more sessions
|
|
1555
|
-
</>
|
|
1556
|
-
)}
|
|
1557
|
-
</Button>
|
|
1558
|
-
)}
|
|
1559
|
-
|
|
1560
|
-
{/* New Session Button */}
|
|
1561
|
-
<div className="md:hidden px-3 pb-2">
|
|
1562
|
-
<button
|
|
1563
|
-
className="w-full h-8 bg-primary hover:bg-primary/90 text-primary-foreground rounded-md flex items-center justify-center gap-2 font-medium text-xs active:scale-[0.98] transition-all duration-150"
|
|
1564
|
-
onClick={() => {
|
|
1565
|
-
handleProjectSelect(project);
|
|
1566
|
-
onNewSession(project);
|
|
1567
|
-
}}
|
|
1568
|
-
>
|
|
1569
|
-
<Plus className="w-3 h-3" />
|
|
1570
|
-
New Session
|
|
1571
|
-
</button>
|
|
1572
|
-
</div>
|
|
1573
|
-
|
|
1574
|
-
<Button
|
|
1575
|
-
variant="default"
|
|
1576
|
-
size="sm"
|
|
1577
|
-
className="hidden md:flex w-full justify-start gap-2 mt-1 h-8 text-xs font-medium bg-primary hover:bg-primary/90 text-primary-foreground transition-colors"
|
|
1578
|
-
onClick={() => onNewSession(project)}
|
|
1579
|
-
>
|
|
1580
|
-
<Plus className="w-3 h-3" />
|
|
1581
|
-
New Session
|
|
1582
|
-
</Button>
|
|
1583
|
-
</div>
|
|
1584
|
-
)}
|
|
1585
|
-
</div>
|
|
1586
|
-
);
|
|
1587
|
-
})
|
|
1588
|
-
)}
|
|
1589
|
-
</div>
|
|
1590
|
-
</ScrollArea>
|
|
1591
|
-
|
|
1592
|
-
{/* Version Update Notification */}
|
|
1593
|
-
{updateAvailable && (
|
|
1594
|
-
<div className="md:p-2 border-t border-border/50 flex-shrink-0">
|
|
1595
|
-
{/* Desktop Version Notification */}
|
|
1596
|
-
<div className="hidden md:block">
|
|
1597
|
-
<Button
|
|
1598
|
-
variant="ghost"
|
|
1599
|
-
className="w-full justify-start gap-3 p-3 h-auto font-normal text-left hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors duration-200 border border-blue-200 dark:border-blue-700 rounded-lg mb-2"
|
|
1600
|
-
onClick={onShowVersionModal}
|
|
1601
|
-
>
|
|
1602
|
-
<div className="relative">
|
|
1603
|
-
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1604
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
|
1605
|
-
</svg>
|
|
1606
|
-
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
|
1607
|
-
</div>
|
|
1608
|
-
<div className="min-w-0 flex-1">
|
|
1609
|
-
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
|
1610
|
-
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
|
1611
|
-
</div>
|
|
1612
|
-
</Button>
|
|
1613
|
-
</div>
|
|
1614
|
-
|
|
1615
|
-
{/* Mobile Version Notification */}
|
|
1616
|
-
<div className="md:hidden p-3 pb-2">
|
|
1617
|
-
<button
|
|
1618
|
-
className="w-full h-12 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-xl flex items-center justify-start gap-3 px-4 active:scale-[0.98] transition-all duration-150"
|
|
1619
|
-
onClick={onShowVersionModal}
|
|
1620
|
-
>
|
|
1621
|
-
<div className="relative">
|
|
1622
|
-
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1623
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
|
1624
|
-
</svg>
|
|
1625
|
-
<div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
|
1626
|
-
</div>
|
|
1627
|
-
<div className="min-w-0 flex-1 text-left">
|
|
1628
|
-
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">Update Available</div>
|
|
1629
|
-
<div className="text-xs text-blue-600 dark:text-blue-400">Version {latestVersion} is ready</div>
|
|
1630
|
-
</div>
|
|
1631
|
-
</button>
|
|
1632
|
-
</div>
|
|
1633
|
-
</div>
|
|
1634
|
-
)}
|
|
1635
|
-
|
|
1636
|
-
{/* Settings Section */}
|
|
1637
|
-
<div className="md:p-2 md:border-t md:border-border flex-shrink-0">
|
|
1638
|
-
{/* Mobile Settings */}
|
|
1639
|
-
<div className="md:hidden p-4 pb-20 border-t border-border/50">
|
|
1640
|
-
<button
|
|
1641
|
-
className="w-full h-14 bg-muted/50 hover:bg-muted/70 rounded-2xl flex items-center justify-start gap-4 px-4 active:scale-[0.98] transition-all duration-150"
|
|
1642
|
-
onClick={onShowSettings}
|
|
1643
|
-
>
|
|
1644
|
-
<div className="w-10 h-10 rounded-2xl bg-background/80 flex items-center justify-center">
|
|
1645
|
-
<Settings className="w-5 h-5 text-muted-foreground" />
|
|
1646
|
-
</div>
|
|
1647
|
-
<span className="text-lg font-medium text-foreground">Settings</span>
|
|
1648
|
-
</button>
|
|
1649
|
-
</div>
|
|
1650
|
-
|
|
1651
|
-
{/* Desktop Settings */}
|
|
1652
|
-
<Button
|
|
1653
|
-
variant="ghost"
|
|
1654
|
-
className="hidden md:flex w-full justify-start gap-2 p-2 h-auto font-normal text-muted-foreground hover:text-foreground hover:bg-accent transition-colors duration-200"
|
|
1655
|
-
onClick={onShowSettings}
|
|
1656
|
-
>
|
|
1657
|
-
<Settings className="w-3 h-3" />
|
|
1658
|
-
<span className="text-xs">Settings</span>
|
|
1659
|
-
</Button>
|
|
1660
|
-
</div>
|
|
1661
|
-
</div>
|
|
1662
|
-
);
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
export default Sidebar;
|