@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
package/src/App.jsx
DELETED
|
@@ -1,751 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* App.jsx - Main Application Component with Session Protection System
|
|
3
|
-
*
|
|
4
|
-
* SESSION PROTECTION SYSTEM OVERVIEW:
|
|
5
|
-
* ===================================
|
|
6
|
-
*
|
|
7
|
-
* Problem: Automatic project updates from WebSocket would refresh the sidebar and clear chat messages
|
|
8
|
-
* during active conversations, creating a poor user experience.
|
|
9
|
-
*
|
|
10
|
-
* Solution: Track "active sessions" and pause project updates during conversations.
|
|
11
|
-
*
|
|
12
|
-
* How it works:
|
|
13
|
-
* 1. When user sends message → session marked as "active"
|
|
14
|
-
* 2. Project updates are skipped while session is active
|
|
15
|
-
* 3. When conversation completes/aborts → session marked as "inactive"
|
|
16
|
-
* 4. Project updates resume normally
|
|
17
|
-
*
|
|
18
|
-
* Handles both existing sessions (with real IDs) and new sessions (with temporary IDs).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import React, { useState, useEffect } from 'react';
|
|
22
|
-
import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom';
|
|
23
|
-
import Sidebar from './components/Sidebar';
|
|
24
|
-
import MainContent from './components/MainContent';
|
|
25
|
-
import MobileNav from './components/MobileNav';
|
|
26
|
-
import Settings from './components/Settings';
|
|
27
|
-
import QuickSettingsPanel from './components/QuickSettingsPanel';
|
|
28
|
-
|
|
29
|
-
import { ThemeProvider } from './contexts/ThemeContext';
|
|
30
|
-
import { AuthProvider } from './contexts/AuthContext';
|
|
31
|
-
import { TaskMasterProvider } from './contexts/TaskMasterContext';
|
|
32
|
-
import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
|
33
|
-
import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext';
|
|
34
|
-
import ProtectedRoute from './components/ProtectedRoute';
|
|
35
|
-
import { useVersionCheck } from './hooks/useVersionCheck';
|
|
36
|
-
import { api, authenticatedFetch } from './utils/api';
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Main App component with routing
|
|
40
|
-
function AppContent() {
|
|
41
|
-
const navigate = useNavigate();
|
|
42
|
-
const { sessionId } = useParams();
|
|
43
|
-
|
|
44
|
-
const { updateAvailable, latestVersion, currentVersion } = useVersionCheck('siteboon', 'claudecodeui');
|
|
45
|
-
const [showVersionModal, setShowVersionModal] = useState(false);
|
|
46
|
-
|
|
47
|
-
const [projects, setProjects] = useState([]);
|
|
48
|
-
const [selectedProject, setSelectedProject] = useState(null);
|
|
49
|
-
const [selectedSession, setSelectedSession] = useState(null);
|
|
50
|
-
const [activeTab, setActiveTab] = useState('chat'); // 'chat' or 'files'
|
|
51
|
-
const [isMobile, setIsMobile] = useState(false);
|
|
52
|
-
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
53
|
-
const [isLoadingProjects, setIsLoadingProjects] = useState(true);
|
|
54
|
-
const [isInputFocused, setIsInputFocused] = useState(false);
|
|
55
|
-
const [showSettings, setShowSettings] = useState(false);
|
|
56
|
-
const [showQuickSettings, setShowQuickSettings] = useState(false);
|
|
57
|
-
const [autoExpandTools, setAutoExpandTools] = useState(() => {
|
|
58
|
-
const saved = localStorage.getItem('autoExpandTools');
|
|
59
|
-
return saved !== null ? JSON.parse(saved) : false;
|
|
60
|
-
});
|
|
61
|
-
const [showRawParameters, setShowRawParameters] = useState(() => {
|
|
62
|
-
const saved = localStorage.getItem('showRawParameters');
|
|
63
|
-
return saved !== null ? JSON.parse(saved) : false;
|
|
64
|
-
});
|
|
65
|
-
const [autoScrollToBottom, setAutoScrollToBottom] = useState(() => {
|
|
66
|
-
const saved = localStorage.getItem('autoScrollToBottom');
|
|
67
|
-
return saved !== null ? JSON.parse(saved) : true;
|
|
68
|
-
});
|
|
69
|
-
const [sendByCtrlEnter, setSendByCtrlEnter] = useState(() => {
|
|
70
|
-
const saved = localStorage.getItem('sendByCtrlEnter');
|
|
71
|
-
return saved !== null ? JSON.parse(saved) : false;
|
|
72
|
-
});
|
|
73
|
-
// Session Protection System: Track sessions with active conversations to prevent
|
|
74
|
-
// automatic project updates from interrupting ongoing chats. When a user sends
|
|
75
|
-
// a message, the session is marked as "active" and project updates are paused
|
|
76
|
-
// until the conversation completes or is aborted.
|
|
77
|
-
const [activeSessions, setActiveSessions] = useState(new Set()); // Track sessions with active conversations
|
|
78
|
-
|
|
79
|
-
const { ws, sendMessage, messages } = useWebSocketContext();
|
|
80
|
-
|
|
81
|
-
// Detect if running as PWA
|
|
82
|
-
const [isPWA, setIsPWA] = useState(false);
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
// Check if running in standalone mode (PWA)
|
|
86
|
-
const checkPWA = () => {
|
|
87
|
-
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
|
|
88
|
-
window.navigator.standalone ||
|
|
89
|
-
document.referrer.includes('android-app://');
|
|
90
|
-
setIsPWA(isStandalone);
|
|
91
|
-
|
|
92
|
-
// Add class to html and body for CSS targeting
|
|
93
|
-
if (isStandalone) {
|
|
94
|
-
document.documentElement.classList.add('pwa-mode');
|
|
95
|
-
document.body.classList.add('pwa-mode');
|
|
96
|
-
} else {
|
|
97
|
-
document.documentElement.classList.remove('pwa-mode');
|
|
98
|
-
document.body.classList.remove('pwa-mode');
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
checkPWA();
|
|
103
|
-
|
|
104
|
-
// Listen for changes
|
|
105
|
-
window.matchMedia('(display-mode: standalone)').addEventListener('change', checkPWA);
|
|
106
|
-
|
|
107
|
-
return () => {
|
|
108
|
-
window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkPWA);
|
|
109
|
-
};
|
|
110
|
-
}, []);
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
const checkMobile = () => {
|
|
114
|
-
setIsMobile(window.innerWidth < 768);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
checkMobile();
|
|
118
|
-
window.addEventListener('resize', checkMobile);
|
|
119
|
-
|
|
120
|
-
return () => window.removeEventListener('resize', checkMobile);
|
|
121
|
-
}, []);
|
|
122
|
-
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
// Fetch projects on component mount
|
|
125
|
-
fetchProjects();
|
|
126
|
-
}, []);
|
|
127
|
-
|
|
128
|
-
// Helper function to determine if an update is purely additive (new sessions/projects)
|
|
129
|
-
// vs modifying existing selected items that would interfere with active conversations
|
|
130
|
-
const isUpdateAdditive = (currentProjects, updatedProjects, selectedProject, selectedSession) => {
|
|
131
|
-
if (!selectedProject || !selectedSession) {
|
|
132
|
-
// No active session to protect, allow all updates
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Find the selected project in both current and updated data
|
|
137
|
-
const currentSelectedProject = currentProjects?.find(p => p.name === selectedProject.name);
|
|
138
|
-
const updatedSelectedProject = updatedProjects?.find(p => p.name === selectedProject.name);
|
|
139
|
-
|
|
140
|
-
if (!currentSelectedProject || !updatedSelectedProject) {
|
|
141
|
-
// Project structure changed significantly, not purely additive
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Find the selected session in both current and updated project data
|
|
146
|
-
const currentSelectedSession = currentSelectedProject.sessions?.find(s => s.id === selectedSession.id);
|
|
147
|
-
const updatedSelectedSession = updatedSelectedProject.sessions?.find(s => s.id === selectedSession.id);
|
|
148
|
-
|
|
149
|
-
if (!currentSelectedSession || !updatedSelectedSession) {
|
|
150
|
-
// Selected session was deleted or significantly changed, not purely additive
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check if the selected session's content has changed (modification vs addition)
|
|
155
|
-
// Compare key fields that would affect the loaded chat interface
|
|
156
|
-
const sessionUnchanged =
|
|
157
|
-
currentSelectedSession.id === updatedSelectedSession.id &&
|
|
158
|
-
currentSelectedSession.title === updatedSelectedSession.title &&
|
|
159
|
-
currentSelectedSession.created_at === updatedSelectedSession.created_at &&
|
|
160
|
-
currentSelectedSession.updated_at === updatedSelectedSession.updated_at;
|
|
161
|
-
|
|
162
|
-
// This is considered additive if the selected session is unchanged
|
|
163
|
-
// (new sessions may have been added elsewhere, but active session is protected)
|
|
164
|
-
return sessionUnchanged;
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Handle WebSocket messages for real-time project updates
|
|
168
|
-
useEffect(() => {
|
|
169
|
-
if (messages.length > 0) {
|
|
170
|
-
const latestMessage = messages[messages.length - 1];
|
|
171
|
-
|
|
172
|
-
if (latestMessage.type === 'projects_updated') {
|
|
173
|
-
|
|
174
|
-
// Session Protection Logic: Allow additions but prevent changes during active conversations
|
|
175
|
-
// This allows new sessions/projects to appear in sidebar while protecting active chat messages
|
|
176
|
-
// We check for two types of active sessions:
|
|
177
|
-
// 1. Existing sessions: selectedSession.id exists in activeSessions
|
|
178
|
-
// 2. New sessions: temporary "new-session-*" identifiers in activeSessions (before real session ID is received)
|
|
179
|
-
const hasActiveSession = (selectedSession && activeSessions.has(selectedSession.id)) ||
|
|
180
|
-
(activeSessions.size > 0 && Array.from(activeSessions).some(id => id.startsWith('new-session-')));
|
|
181
|
-
|
|
182
|
-
if (hasActiveSession) {
|
|
183
|
-
// Allow updates but be selective: permit additions, prevent changes to existing items
|
|
184
|
-
const updatedProjects = latestMessage.projects;
|
|
185
|
-
const currentProjects = projects;
|
|
186
|
-
|
|
187
|
-
// Check if this is purely additive (new sessions/projects) vs modification of existing ones
|
|
188
|
-
const isAdditiveUpdate = isUpdateAdditive(currentProjects, updatedProjects, selectedProject, selectedSession);
|
|
189
|
-
|
|
190
|
-
if (!isAdditiveUpdate) {
|
|
191
|
-
// Skip updates that would modify existing selected session/project
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
// Continue with additive updates below
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Update projects state with the new data from WebSocket
|
|
198
|
-
const updatedProjects = latestMessage.projects;
|
|
199
|
-
setProjects(updatedProjects);
|
|
200
|
-
|
|
201
|
-
// Update selected project if it exists in the updated projects
|
|
202
|
-
if (selectedProject) {
|
|
203
|
-
const updatedSelectedProject = updatedProjects.find(p => p.name === selectedProject.name);
|
|
204
|
-
if (updatedSelectedProject) {
|
|
205
|
-
setSelectedProject(updatedSelectedProject);
|
|
206
|
-
|
|
207
|
-
// Update selected session only if it was deleted - avoid unnecessary reloads
|
|
208
|
-
if (selectedSession) {
|
|
209
|
-
const updatedSelectedSession = updatedSelectedProject.sessions?.find(s => s.id === selectedSession.id);
|
|
210
|
-
if (!updatedSelectedSession) {
|
|
211
|
-
// Session was deleted
|
|
212
|
-
setSelectedSession(null);
|
|
213
|
-
}
|
|
214
|
-
// Don't update if session still exists with same ID - prevents reload
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}, [messages, selectedProject, selectedSession, activeSessions]);
|
|
221
|
-
|
|
222
|
-
const fetchProjects = async () => {
|
|
223
|
-
try {
|
|
224
|
-
setIsLoadingProjects(true);
|
|
225
|
-
const response = await api.projects();
|
|
226
|
-
const data = await response.json();
|
|
227
|
-
|
|
228
|
-
// Always fetch Cursor sessions for each project so we can combine views
|
|
229
|
-
for (let project of data) {
|
|
230
|
-
try {
|
|
231
|
-
const url = `/api/cursor/sessions?projectPath=${encodeURIComponent(project.fullPath || project.path)}`;
|
|
232
|
-
const cursorResponse = await authenticatedFetch(url);
|
|
233
|
-
if (cursorResponse.ok) {
|
|
234
|
-
const cursorData = await cursorResponse.json();
|
|
235
|
-
if (cursorData.success && cursorData.sessions) {
|
|
236
|
-
project.cursorSessions = cursorData.sessions;
|
|
237
|
-
} else {
|
|
238
|
-
project.cursorSessions = [];
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
project.cursorSessions = [];
|
|
242
|
-
}
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.error(`Error fetching Cursor sessions for project ${project.name}:`, error);
|
|
245
|
-
project.cursorSessions = [];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Optimize to preserve object references when data hasn't changed
|
|
250
|
-
setProjects(prevProjects => {
|
|
251
|
-
// If no previous projects, just set the new data
|
|
252
|
-
if (prevProjects.length === 0) {
|
|
253
|
-
return data;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Check if the projects data has actually changed
|
|
257
|
-
const hasChanges = data.some((newProject, index) => {
|
|
258
|
-
const prevProject = prevProjects[index];
|
|
259
|
-
if (!prevProject) return true;
|
|
260
|
-
|
|
261
|
-
// Compare key properties that would affect UI
|
|
262
|
-
return (
|
|
263
|
-
newProject.name !== prevProject.name ||
|
|
264
|
-
newProject.displayName !== prevProject.displayName ||
|
|
265
|
-
newProject.fullPath !== prevProject.fullPath ||
|
|
266
|
-
JSON.stringify(newProject.sessionMeta) !== JSON.stringify(prevProject.sessionMeta) ||
|
|
267
|
-
JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions) ||
|
|
268
|
-
JSON.stringify(newProject.cursorSessions) !== JSON.stringify(prevProject.cursorSessions)
|
|
269
|
-
);
|
|
270
|
-
}) || data.length !== prevProjects.length;
|
|
271
|
-
|
|
272
|
-
// Only update if there are actual changes
|
|
273
|
-
return hasChanges ? data : prevProjects;
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Don't auto-select any project - user should choose manually
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.error('Error fetching projects:', error);
|
|
279
|
-
} finally {
|
|
280
|
-
setIsLoadingProjects(false);
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Expose fetchProjects globally for component access
|
|
285
|
-
window.refreshProjects = fetchProjects;
|
|
286
|
-
|
|
287
|
-
// Handle URL-based session loading
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
if (sessionId && projects.length > 0) {
|
|
290
|
-
// Only switch tabs on initial load, not on every project update
|
|
291
|
-
const shouldSwitchTab = !selectedSession || selectedSession.id !== sessionId;
|
|
292
|
-
// Find the session across all projects
|
|
293
|
-
for (const project of projects) {
|
|
294
|
-
let session = project.sessions?.find(s => s.id === sessionId);
|
|
295
|
-
if (session) {
|
|
296
|
-
setSelectedProject(project);
|
|
297
|
-
setSelectedSession({ ...session, __provider: 'claude' });
|
|
298
|
-
// Only switch to chat tab if we're loading a different session
|
|
299
|
-
if (shouldSwitchTab) {
|
|
300
|
-
setActiveTab('chat');
|
|
301
|
-
}
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
// Also check Cursor sessions
|
|
305
|
-
const cSession = project.cursorSessions?.find(s => s.id === sessionId);
|
|
306
|
-
if (cSession) {
|
|
307
|
-
setSelectedProject(project);
|
|
308
|
-
setSelectedSession({ ...cSession, __provider: 'cursor' });
|
|
309
|
-
if (shouldSwitchTab) {
|
|
310
|
-
setActiveTab('chat');
|
|
311
|
-
}
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// If session not found, it might be a newly created session
|
|
317
|
-
// Just navigate to it and it will be found when the sidebar refreshes
|
|
318
|
-
// Don't redirect to home, let the session load naturally
|
|
319
|
-
}
|
|
320
|
-
}, [sessionId, projects, navigate]);
|
|
321
|
-
|
|
322
|
-
const handleProjectSelect = (project) => {
|
|
323
|
-
setSelectedProject(project);
|
|
324
|
-
setSelectedSession(null);
|
|
325
|
-
navigate('/');
|
|
326
|
-
if (isMobile) {
|
|
327
|
-
setSidebarOpen(false);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const handleSessionSelect = (session) => {
|
|
332
|
-
setSelectedSession(session);
|
|
333
|
-
// Only switch to chat tab when user explicitly selects a session
|
|
334
|
-
// This prevents tab switching during automatic updates
|
|
335
|
-
if (activeTab !== 'git' && activeTab !== 'preview') {
|
|
336
|
-
setActiveTab('chat');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// For Cursor sessions, we need to set the session ID differently
|
|
340
|
-
// since they're persistent and not created by Claude
|
|
341
|
-
const provider = localStorage.getItem('selected-provider') || 'claude';
|
|
342
|
-
if (provider === 'cursor') {
|
|
343
|
-
// Cursor sessions have persistent IDs
|
|
344
|
-
sessionStorage.setItem('cursorSessionId', session.id);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (isMobile) {
|
|
348
|
-
setSidebarOpen(false);
|
|
349
|
-
}
|
|
350
|
-
navigate(`/session/${session.id}`);
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const handleNewSession = (project) => {
|
|
354
|
-
setSelectedProject(project);
|
|
355
|
-
setSelectedSession(null);
|
|
356
|
-
setActiveTab('chat');
|
|
357
|
-
navigate('/');
|
|
358
|
-
if (isMobile) {
|
|
359
|
-
setSidebarOpen(false);
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
const handleSessionDelete = (sessionId) => {
|
|
364
|
-
// If the deleted session was currently selected, clear it
|
|
365
|
-
if (selectedSession?.id === sessionId) {
|
|
366
|
-
setSelectedSession(null);
|
|
367
|
-
navigate('/');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Update projects state locally instead of full refresh
|
|
371
|
-
setProjects(prevProjects =>
|
|
372
|
-
prevProjects.map(project => ({
|
|
373
|
-
...project,
|
|
374
|
-
sessions: project.sessions?.filter(session => session.id !== sessionId) || [],
|
|
375
|
-
sessionMeta: {
|
|
376
|
-
...project.sessionMeta,
|
|
377
|
-
total: Math.max(0, (project.sessionMeta?.total || 0) - 1)
|
|
378
|
-
}
|
|
379
|
-
}))
|
|
380
|
-
);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const handleSidebarRefresh = async () => {
|
|
386
|
-
// Refresh only the sessions for all projects, don't change selected state
|
|
387
|
-
try {
|
|
388
|
-
const response = await api.projects();
|
|
389
|
-
const freshProjects = await response.json();
|
|
390
|
-
|
|
391
|
-
// Optimize to preserve object references and minimize re-renders
|
|
392
|
-
setProjects(prevProjects => {
|
|
393
|
-
// Check if projects data has actually changed
|
|
394
|
-
const hasChanges = freshProjects.some((newProject, index) => {
|
|
395
|
-
const prevProject = prevProjects[index];
|
|
396
|
-
if (!prevProject) return true;
|
|
397
|
-
|
|
398
|
-
return (
|
|
399
|
-
newProject.name !== prevProject.name ||
|
|
400
|
-
newProject.displayName !== prevProject.displayName ||
|
|
401
|
-
newProject.fullPath !== prevProject.fullPath ||
|
|
402
|
-
JSON.stringify(newProject.sessionMeta) !== JSON.stringify(prevProject.sessionMeta) ||
|
|
403
|
-
JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions)
|
|
404
|
-
);
|
|
405
|
-
}) || freshProjects.length !== prevProjects.length;
|
|
406
|
-
|
|
407
|
-
return hasChanges ? freshProjects : prevProjects;
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// If we have a selected project, make sure it's still selected after refresh
|
|
411
|
-
if (selectedProject) {
|
|
412
|
-
const refreshedProject = freshProjects.find(p => p.name === selectedProject.name);
|
|
413
|
-
if (refreshedProject) {
|
|
414
|
-
// Only update selected project if it actually changed
|
|
415
|
-
if (JSON.stringify(refreshedProject) !== JSON.stringify(selectedProject)) {
|
|
416
|
-
setSelectedProject(refreshedProject);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// If we have a selected session, try to find it in the refreshed project
|
|
420
|
-
if (selectedSession) {
|
|
421
|
-
const refreshedSession = refreshedProject.sessions?.find(s => s.id === selectedSession.id);
|
|
422
|
-
if (refreshedSession && JSON.stringify(refreshedSession) !== JSON.stringify(selectedSession)) {
|
|
423
|
-
setSelectedSession(refreshedSession);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error('Error refreshing sidebar:', error);
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const handleProjectDelete = (projectName) => {
|
|
434
|
-
// If the deleted project was currently selected, clear it
|
|
435
|
-
if (selectedProject?.name === projectName) {
|
|
436
|
-
setSelectedProject(null);
|
|
437
|
-
setSelectedSession(null);
|
|
438
|
-
navigate('/');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Update projects state locally instead of full refresh
|
|
442
|
-
setProjects(prevProjects =>
|
|
443
|
-
prevProjects.filter(project => project.name !== projectName)
|
|
444
|
-
);
|
|
445
|
-
};
|
|
446
|
-
|
|
447
|
-
// Session Protection Functions: Manage the lifecycle of active sessions
|
|
448
|
-
|
|
449
|
-
// markSessionAsActive: Called when user sends a message to mark session as protected
|
|
450
|
-
// This includes both real session IDs and temporary "new-session-*" identifiers
|
|
451
|
-
const markSessionAsActive = (sessionId) => {
|
|
452
|
-
if (sessionId) {
|
|
453
|
-
setActiveSessions(prev => new Set([...prev, sessionId]));
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
// markSessionAsInactive: Called when conversation completes/aborts to re-enable project updates
|
|
458
|
-
const markSessionAsInactive = (sessionId) => {
|
|
459
|
-
if (sessionId) {
|
|
460
|
-
setActiveSessions(prev => {
|
|
461
|
-
const newSet = new Set(prev);
|
|
462
|
-
newSet.delete(sessionId);
|
|
463
|
-
return newSet;
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
// replaceTemporarySession: Called when WebSocket provides real session ID for new sessions
|
|
469
|
-
// Removes temporary "new-session-*" identifiers and adds the real session ID
|
|
470
|
-
// This maintains protection continuity during the transition from temporary to real session
|
|
471
|
-
const replaceTemporarySession = (realSessionId) => {
|
|
472
|
-
if (realSessionId) {
|
|
473
|
-
setActiveSessions(prev => {
|
|
474
|
-
const newSet = new Set();
|
|
475
|
-
// Keep all non-temporary sessions and add the real session ID
|
|
476
|
-
for (const sessionId of prev) {
|
|
477
|
-
if (!sessionId.startsWith('new-session-')) {
|
|
478
|
-
newSet.add(sessionId);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
newSet.add(realSessionId);
|
|
482
|
-
return newSet;
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// Version Upgrade Modal Component
|
|
488
|
-
const VersionUpgradeModal = () => {
|
|
489
|
-
if (!showVersionModal) return null;
|
|
490
|
-
|
|
491
|
-
return (
|
|
492
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
493
|
-
{/* Backdrop */}
|
|
494
|
-
<div
|
|
495
|
-
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
|
|
496
|
-
onClick={() => setShowVersionModal(false)}
|
|
497
|
-
/>
|
|
498
|
-
|
|
499
|
-
{/* Modal */}
|
|
500
|
-
<div className="relative bg-white dark:bg-gray-800 rounded-lg shadow-xl border border-gray-200 dark:border-gray-700 w-full max-w-md mx-4 p-6 space-y-4">
|
|
501
|
-
{/* Header */}
|
|
502
|
-
<div className="flex items-center justify-between">
|
|
503
|
-
<div className="flex items-center gap-3">
|
|
504
|
-
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
|
505
|
-
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
506
|
-
<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" />
|
|
507
|
-
</svg>
|
|
508
|
-
</div>
|
|
509
|
-
<div>
|
|
510
|
-
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Update Available</h2>
|
|
511
|
-
<p className="text-sm text-gray-500 dark:text-gray-400">A new version is ready</p>
|
|
512
|
-
</div>
|
|
513
|
-
</div>
|
|
514
|
-
<button
|
|
515
|
-
onClick={() => setShowVersionModal(false)}
|
|
516
|
-
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
517
|
-
>
|
|
518
|
-
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
519
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
520
|
-
</svg>
|
|
521
|
-
</button>
|
|
522
|
-
</div>
|
|
523
|
-
|
|
524
|
-
{/* Version Info */}
|
|
525
|
-
<div className="space-y-3">
|
|
526
|
-
<div className="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
|
527
|
-
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Current Version</span>
|
|
528
|
-
<span className="text-sm text-gray-900 dark:text-white font-mono">{currentVersion}</span>
|
|
529
|
-
</div>
|
|
530
|
-
<div className="flex justify-between items-center p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700">
|
|
531
|
-
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">Latest Version</span>
|
|
532
|
-
<span className="text-sm text-blue-900 dark:text-blue-100 font-mono">{latestVersion}</span>
|
|
533
|
-
</div>
|
|
534
|
-
</div>
|
|
535
|
-
|
|
536
|
-
{/* Upgrade Instructions */}
|
|
537
|
-
<div className="space-y-3">
|
|
538
|
-
<h3 className="text-sm font-medium text-gray-900 dark:text-white">How to upgrade:</h3>
|
|
539
|
-
<div className="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 border">
|
|
540
|
-
<code className="text-sm text-gray-800 dark:text-gray-200 font-mono">
|
|
541
|
-
git checkout main && git pull && npm install
|
|
542
|
-
</code>
|
|
543
|
-
</div>
|
|
544
|
-
<p className="text-xs text-gray-600 dark:text-gray-400">
|
|
545
|
-
Run this command in your Claude Code UI directory to update to the latest version.
|
|
546
|
-
</p>
|
|
547
|
-
</div>
|
|
548
|
-
|
|
549
|
-
{/* Actions */}
|
|
550
|
-
<div className="flex gap-2 pt-2">
|
|
551
|
-
<button
|
|
552
|
-
onClick={() => setShowVersionModal(false)}
|
|
553
|
-
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors"
|
|
554
|
-
>
|
|
555
|
-
Later
|
|
556
|
-
</button>
|
|
557
|
-
<button
|
|
558
|
-
onClick={() => {
|
|
559
|
-
// Copy command to clipboard
|
|
560
|
-
navigator.clipboard.writeText('git checkout main && git pull && npm install');
|
|
561
|
-
setShowVersionModal(false);
|
|
562
|
-
}}
|
|
563
|
-
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
564
|
-
>
|
|
565
|
-
Copy Command
|
|
566
|
-
</button>
|
|
567
|
-
</div>
|
|
568
|
-
</div>
|
|
569
|
-
</div>
|
|
570
|
-
);
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
return (
|
|
574
|
-
<div className="fixed inset-0 flex bg-background">
|
|
575
|
-
{/* Fixed Desktop Sidebar */}
|
|
576
|
-
{!isMobile && (
|
|
577
|
-
<div className="w-80 flex-shrink-0 border-r border-border bg-card">
|
|
578
|
-
<div className="h-full overflow-hidden">
|
|
579
|
-
<Sidebar
|
|
580
|
-
projects={projects}
|
|
581
|
-
selectedProject={selectedProject}
|
|
582
|
-
selectedSession={selectedSession}
|
|
583
|
-
onProjectSelect={handleProjectSelect}
|
|
584
|
-
onSessionSelect={handleSessionSelect}
|
|
585
|
-
onNewSession={handleNewSession}
|
|
586
|
-
onSessionDelete={handleSessionDelete}
|
|
587
|
-
onProjectDelete={handleProjectDelete}
|
|
588
|
-
isLoading={isLoadingProjects}
|
|
589
|
-
onRefresh={handleSidebarRefresh}
|
|
590
|
-
onShowSettings={() => setShowSettings(true)}
|
|
591
|
-
updateAvailable={updateAvailable}
|
|
592
|
-
latestVersion={latestVersion}
|
|
593
|
-
currentVersion={currentVersion}
|
|
594
|
-
onShowVersionModal={() => setShowVersionModal(true)}
|
|
595
|
-
isPWA={isPWA}
|
|
596
|
-
isMobile={isMobile}
|
|
597
|
-
/>
|
|
598
|
-
</div>
|
|
599
|
-
</div>
|
|
600
|
-
)}
|
|
601
|
-
|
|
602
|
-
{/* Mobile Sidebar Overlay */}
|
|
603
|
-
{isMobile && (
|
|
604
|
-
<div className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${
|
|
605
|
-
sidebarOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
|
|
606
|
-
}`}>
|
|
607
|
-
<div
|
|
608
|
-
className="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
|
|
609
|
-
onClick={(e) => {
|
|
610
|
-
e.stopPropagation();
|
|
611
|
-
setSidebarOpen(false);
|
|
612
|
-
}}
|
|
613
|
-
onTouchStart={(e) => {
|
|
614
|
-
e.preventDefault();
|
|
615
|
-
e.stopPropagation();
|
|
616
|
-
setSidebarOpen(false);
|
|
617
|
-
}}
|
|
618
|
-
/>
|
|
619
|
-
<div
|
|
620
|
-
className={`relative w-[85vw] max-w-sm sm:w-80 bg-card border-r border-border transform transition-transform duration-150 ease-out ${
|
|
621
|
-
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
622
|
-
}`}
|
|
623
|
-
style={{ height: 'calc(100vh - 80px)' }}
|
|
624
|
-
onClick={(e) => e.stopPropagation()}
|
|
625
|
-
onTouchStart={(e) => e.stopPropagation()}
|
|
626
|
-
>
|
|
627
|
-
<Sidebar
|
|
628
|
-
projects={projects}
|
|
629
|
-
selectedProject={selectedProject}
|
|
630
|
-
selectedSession={selectedSession}
|
|
631
|
-
onProjectSelect={handleProjectSelect}
|
|
632
|
-
onSessionSelect={handleSessionSelect}
|
|
633
|
-
onNewSession={handleNewSession}
|
|
634
|
-
onSessionDelete={handleSessionDelete}
|
|
635
|
-
onProjectDelete={handleProjectDelete}
|
|
636
|
-
isLoading={isLoadingProjects}
|
|
637
|
-
onRefresh={handleSidebarRefresh}
|
|
638
|
-
onShowSettings={() => setShowSettings(true)}
|
|
639
|
-
updateAvailable={updateAvailable}
|
|
640
|
-
latestVersion={latestVersion}
|
|
641
|
-
currentVersion={currentVersion}
|
|
642
|
-
onShowVersionModal={() => setShowVersionModal(true)}
|
|
643
|
-
isPWA={isPWA}
|
|
644
|
-
isMobile={isMobile}
|
|
645
|
-
/>
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
)}
|
|
649
|
-
|
|
650
|
-
{/* Main Content Area - Flexible */}
|
|
651
|
-
<div className={`flex-1 flex flex-col min-w-0 ${isMobile && !isInputFocused ? 'pb-16' : ''}`}>
|
|
652
|
-
<MainContent
|
|
653
|
-
selectedProject={selectedProject}
|
|
654
|
-
selectedSession={selectedSession}
|
|
655
|
-
activeTab={activeTab}
|
|
656
|
-
setActiveTab={setActiveTab}
|
|
657
|
-
ws={ws}
|
|
658
|
-
sendMessage={sendMessage}
|
|
659
|
-
messages={messages}
|
|
660
|
-
isMobile={isMobile}
|
|
661
|
-
isPWA={isPWA}
|
|
662
|
-
onMenuClick={() => setSidebarOpen(true)}
|
|
663
|
-
isLoading={isLoadingProjects}
|
|
664
|
-
onInputFocusChange={setIsInputFocused}
|
|
665
|
-
onSessionActive={markSessionAsActive}
|
|
666
|
-
onSessionInactive={markSessionAsInactive}
|
|
667
|
-
onReplaceTemporarySession={replaceTemporarySession}
|
|
668
|
-
onNavigateToSession={(sessionId) => navigate(`/session/${sessionId}`)}
|
|
669
|
-
onShowSettings={() => setShowSettings(true)}
|
|
670
|
-
autoExpandTools={autoExpandTools}
|
|
671
|
-
showRawParameters={showRawParameters}
|
|
672
|
-
autoScrollToBottom={autoScrollToBottom}
|
|
673
|
-
sendByCtrlEnter={sendByCtrlEnter}
|
|
674
|
-
/>
|
|
675
|
-
</div>
|
|
676
|
-
|
|
677
|
-
{/* Mobile Bottom Navigation */}
|
|
678
|
-
{isMobile && (
|
|
679
|
-
<MobileNav
|
|
680
|
-
activeTab={activeTab}
|
|
681
|
-
setActiveTab={setActiveTab}
|
|
682
|
-
isInputFocused={isInputFocused}
|
|
683
|
-
/>
|
|
684
|
-
)}
|
|
685
|
-
{/* Quick Settings Panel - Only show on chat tab */}
|
|
686
|
-
{activeTab === 'chat' && (
|
|
687
|
-
<QuickSettingsPanel
|
|
688
|
-
isOpen={showQuickSettings}
|
|
689
|
-
onToggle={setShowQuickSettings}
|
|
690
|
-
autoExpandTools={autoExpandTools}
|
|
691
|
-
onAutoExpandChange={(value) => {
|
|
692
|
-
setAutoExpandTools(value);
|
|
693
|
-
localStorage.setItem('autoExpandTools', JSON.stringify(value));
|
|
694
|
-
}}
|
|
695
|
-
showRawParameters={showRawParameters}
|
|
696
|
-
onShowRawParametersChange={(value) => {
|
|
697
|
-
setShowRawParameters(value);
|
|
698
|
-
localStorage.setItem('showRawParameters', JSON.stringify(value));
|
|
699
|
-
}}
|
|
700
|
-
autoScrollToBottom={autoScrollToBottom}
|
|
701
|
-
onAutoScrollChange={(value) => {
|
|
702
|
-
setAutoScrollToBottom(value);
|
|
703
|
-
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
|
|
704
|
-
}}
|
|
705
|
-
sendByCtrlEnter={sendByCtrlEnter}
|
|
706
|
-
onSendByCtrlEnterChange={(value) => {
|
|
707
|
-
setSendByCtrlEnter(value);
|
|
708
|
-
localStorage.setItem('sendByCtrlEnter', JSON.stringify(value));
|
|
709
|
-
}}
|
|
710
|
-
isMobile={isMobile}
|
|
711
|
-
/>
|
|
712
|
-
)}
|
|
713
|
-
|
|
714
|
-
{/* Settings Modal */}
|
|
715
|
-
<Settings
|
|
716
|
-
isOpen={showSettings}
|
|
717
|
-
onClose={() => setShowSettings(false)}
|
|
718
|
-
projects={projects}
|
|
719
|
-
/>
|
|
720
|
-
|
|
721
|
-
{/* Version Upgrade Modal */}
|
|
722
|
-
<VersionUpgradeModal />
|
|
723
|
-
</div>
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Root App component with router
|
|
728
|
-
function App() {
|
|
729
|
-
return (
|
|
730
|
-
<ThemeProvider>
|
|
731
|
-
<AuthProvider>
|
|
732
|
-
<WebSocketProvider>
|
|
733
|
-
<TasksSettingsProvider>
|
|
734
|
-
<TaskMasterProvider>
|
|
735
|
-
<ProtectedRoute>
|
|
736
|
-
<Router>
|
|
737
|
-
<Routes>
|
|
738
|
-
<Route path="/" element={<AppContent />} />
|
|
739
|
-
<Route path="/session/:sessionId" element={<AppContent />} />
|
|
740
|
-
</Routes>
|
|
741
|
-
</Router>
|
|
742
|
-
</ProtectedRoute>
|
|
743
|
-
</TaskMasterProvider>
|
|
744
|
-
</TasksSettingsProvider>
|
|
745
|
-
</WebSocketProvider>
|
|
746
|
-
</AuthProvider>
|
|
747
|
-
</ThemeProvider>
|
|
748
|
-
);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
export default App;
|