@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.
Files changed (92) hide show
  1. package/dist/assets/index-CeR_JfKq.js +895 -0
  2. package/dist/assets/index-Co7ALK3i.css +32 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +6 -1
  5. package/server/database/auth.db +0 -0
  6. package/.env.example +0 -12
  7. package/.nvmrc +0 -1
  8. package/postcss.config.js +0 -6
  9. package/src/App.jsx +0 -751
  10. package/src/components/ChatInterface.jsx +0 -3485
  11. package/src/components/ClaudeLogo.jsx +0 -11
  12. package/src/components/ClaudeStatus.jsx +0 -107
  13. package/src/components/CodeEditor.jsx +0 -422
  14. package/src/components/CreateTaskModal.jsx +0 -88
  15. package/src/components/CursorLogo.jsx +0 -9
  16. package/src/components/DarkModeToggle.jsx +0 -35
  17. package/src/components/DiffViewer.jsx +0 -41
  18. package/src/components/ErrorBoundary.jsx +0 -73
  19. package/src/components/FileTree.jsx +0 -480
  20. package/src/components/GitPanel.jsx +0 -1283
  21. package/src/components/ImageViewer.jsx +0 -54
  22. package/src/components/LoginForm.jsx +0 -110
  23. package/src/components/MainContent.jsx +0 -577
  24. package/src/components/MicButton.jsx +0 -272
  25. package/src/components/MobileNav.jsx +0 -88
  26. package/src/components/NextTaskBanner.jsx +0 -695
  27. package/src/components/PRDEditor.jsx +0 -871
  28. package/src/components/ProtectedRoute.jsx +0 -44
  29. package/src/components/QuickSettingsPanel.jsx +0 -262
  30. package/src/components/Settings.jsx +0 -2023
  31. package/src/components/SetupForm.jsx +0 -135
  32. package/src/components/Shell.jsx +0 -663
  33. package/src/components/Sidebar.jsx +0 -1665
  34. package/src/components/StandaloneShell.jsx +0 -106
  35. package/src/components/TaskCard.jsx +0 -210
  36. package/src/components/TaskDetail.jsx +0 -406
  37. package/src/components/TaskIndicator.jsx +0 -108
  38. package/src/components/TaskList.jsx +0 -1054
  39. package/src/components/TaskMasterSetupWizard.jsx +0 -603
  40. package/src/components/TaskMasterStatus.jsx +0 -86
  41. package/src/components/TodoList.jsx +0 -91
  42. package/src/components/Tooltip.jsx +0 -91
  43. package/src/components/ui/badge.jsx +0 -31
  44. package/src/components/ui/button.jsx +0 -46
  45. package/src/components/ui/input.jsx +0 -19
  46. package/src/components/ui/scroll-area.jsx +0 -23
  47. package/src/contexts/AuthContext.jsx +0 -158
  48. package/src/contexts/TaskMasterContext.jsx +0 -324
  49. package/src/contexts/TasksSettingsContext.jsx +0 -95
  50. package/src/contexts/ThemeContext.jsx +0 -94
  51. package/src/contexts/WebSocketContext.jsx +0 -29
  52. package/src/hooks/useAudioRecorder.js +0 -109
  53. package/src/hooks/useVersionCheck.js +0 -39
  54. package/src/index.css +0 -822
  55. package/src/lib/utils.js +0 -6
  56. package/src/main.jsx +0 -10
  57. package/src/utils/api.js +0 -141
  58. package/src/utils/websocket.js +0 -109
  59. package/src/utils/whisper.js +0 -37
  60. package/tailwind.config.js +0 -63
  61. package/vite.config.js +0 -29
  62. /package/{public → dist}/convert-icons.md +0 -0
  63. /package/{public → dist}/favicon.png +0 -0
  64. /package/{public → dist}/favicon.svg +0 -0
  65. /package/{public → dist}/generate-icons.js +0 -0
  66. /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
  67. /package/{public → dist}/icons/cursor.svg +0 -0
  68. /package/{public → dist}/icons/generate-icons.md +0 -0
  69. /package/{public → dist}/icons/icon-128x128.png +0 -0
  70. /package/{public → dist}/icons/icon-128x128.svg +0 -0
  71. /package/{public → dist}/icons/icon-144x144.png +0 -0
  72. /package/{public → dist}/icons/icon-144x144.svg +0 -0
  73. /package/{public → dist}/icons/icon-152x152.png +0 -0
  74. /package/{public → dist}/icons/icon-152x152.svg +0 -0
  75. /package/{public → dist}/icons/icon-192x192.png +0 -0
  76. /package/{public → dist}/icons/icon-192x192.svg +0 -0
  77. /package/{public → dist}/icons/icon-384x384.png +0 -0
  78. /package/{public → dist}/icons/icon-384x384.svg +0 -0
  79. /package/{public → dist}/icons/icon-512x512.png +0 -0
  80. /package/{public → dist}/icons/icon-512x512.svg +0 -0
  81. /package/{public → dist}/icons/icon-72x72.png +0 -0
  82. /package/{public → dist}/icons/icon-72x72.svg +0 -0
  83. /package/{public → dist}/icons/icon-96x96.png +0 -0
  84. /package/{public → dist}/icons/icon-96x96.svg +0 -0
  85. /package/{public → dist}/icons/icon-template.svg +0 -0
  86. /package/{public → dist}/logo.svg +0 -0
  87. /package/{public → dist}/manifest.json +0 -0
  88. /package/{public → dist}/screenshots/cli-selection.png +0 -0
  89. /package/{public → dist}/screenshots/desktop-main.png +0 -0
  90. /package/{public → dist}/screenshots/mobile-chat.png +0 -0
  91. /package/{public → dist}/screenshots/tools-modal.png +0 -0
  92. /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;