diff --git a/server/constants/config.js b/server/constants/config.js new file mode 100644 index 0000000..580a985 --- /dev/null +++ b/server/constants/config.js @@ -0,0 +1,5 @@ +/** + * Environment Flag: Is Platform + * Indicates if the app is running in Platform mode (hosted) or OSS mode (self-hosted) + */ +export const IS_PLATFORM = process.env.VITE_IS_PLATFORM === 'true'; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 1c42d30..1db726d 100755 --- a/server/index.js +++ b/server/index.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -// Load environment variables from .env file +// Load environment variables before other imports execute +import './load-env.js'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -28,22 +29,6 @@ const c = { dim: (text) => `${colors.dim}${text}${colors.reset}`, }; -try { - const envPath = path.join(__dirname, '../.env'); - const envFile = fs.readFileSync(envPath, 'utf8'); - envFile.split('\n').forEach(line => { - const trimmedLine = line.trim(); - if (trimmedLine && !trimmedLine.startsWith('#')) { - const [key, ...valueParts] = trimmedLine.split('='); - if (key && valueParts.length > 0 && !process.env[key]) { - process.env[key] = valueParts.join('=').trim(); - } - } - }); -} catch (e) { - console.log('No .env file found or error reading it:', e.message); -} - console.log('PORT from env:', process.env.PORT); import express from 'express'; @@ -76,6 +61,7 @@ import userRoutes from './routes/user.js'; import codexRoutes from './routes/codex.js'; import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; +import { IS_PLATFORM } from './constants/config.js'; // File system watcher for projects folder let projectsWatcher = null; @@ -200,7 +186,7 @@ const wss = new WebSocketServer({ console.log('WebSocket connection attempt to:', info.req.url); // Platform mode: always allow connection - if (process.env.VITE_IS_PLATFORM === 'true') { + if (IS_PLATFORM) { const user = authenticateWebSocket(null); // Will return first user if (!user) { console.log('[WARN] Platform mode: No user found in database'); diff --git a/server/load-env.js b/server/load-env.js new file mode 100644 index 0000000..21280a4 --- /dev/null +++ b/server/load-env.js @@ -0,0 +1,24 @@ +// Load environment variables from .env before other imports execute. +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +try { + const envPath = path.join(__dirname, '../.env'); + const envFile = fs.readFileSync(envPath, 'utf8'); + envFile.split('\n').forEach(line => { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const [key, ...valueParts] = trimmedLine.split('='); + if (key && valueParts.length > 0 && !process.env[key]) { + process.env[key] = valueParts.join('=').trim(); + } + } + }); +} catch (e) { + console.log('No .env file found or error reading it:', e.message); +} diff --git a/server/middleware/auth.js b/server/middleware/auth.js index 9231e4e..ab12e0c 100644 --- a/server/middleware/auth.js +++ b/server/middleware/auth.js @@ -1,5 +1,6 @@ import jwt from 'jsonwebtoken'; import { userDb } from '../database/db.js'; +import { IS_PLATFORM } from '../constants/config.js'; // Get JWT secret from environment or use default (for development) const JWT_SECRET = process.env.JWT_SECRET || 'claude-ui-dev-secret-change-in-production'; @@ -21,7 +22,7 @@ const validateApiKey = (req, res, next) => { // JWT authentication middleware const authenticateToken = async (req, res, next) => { // Platform mode: use single database user - if (process.env.VITE_IS_PLATFORM === 'true') { + if (IS_PLATFORM) { try { const user = userDb.getFirstUser(); if (!user) { @@ -80,7 +81,7 @@ const generateToken = (user) => { // WebSocket authentication function const authenticateWebSocket = (token) => { // Platform mode: bypass token validation, return first user - if (process.env.VITE_IS_PLATFORM === 'true') { + if (IS_PLATFORM) { try { const user = userDb.getFirstUser(); if (user) { diff --git a/server/routes/agent.js b/server/routes/agent.js index f633034..3ef2620 100644 --- a/server/routes/agent.js +++ b/server/routes/agent.js @@ -11,6 +11,7 @@ import { spawnCursor } from '../cursor-cli.js'; import { queryCodex } from '../openai-codex.js'; import { Octokit } from '@octokit/rest'; import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js'; +import { IS_PLATFORM } from '../constants/config.js'; const router = express.Router(); @@ -18,7 +19,7 @@ const router = express.Router(); * Middleware to authenticate agent API requests. * * Supports two authentication modes: - * 1. Platform mode (VITE_IS_PLATFORM=true): For managed/hosted deployments where + * 1. Platform mode (IS_PLATFORM=true): For managed/hosted deployments where * authentication is handled by an external proxy. Requests are trusted and * the default user context is used. * @@ -28,7 +29,7 @@ const router = express.Router(); const validateExternalApiKey = (req, res, next) => { // Platform mode: Authentication is handled externally (e.g., by a proxy layer). // Trust the request and use the default user context. - if (process.env.VITE_IS_PLATFORM === 'true') { + if (IS_PLATFORM) { try { const user = userDb.getFirstUser(); if (!user) { diff --git a/shared/modelConstants.js b/shared/modelConstants.js index 7d4347f..a327c9f 100644 --- a/shared/modelConstants.js +++ b/shared/modelConstants.js @@ -62,4 +62,4 @@ export const CODEX_MODELS = { ], DEFAULT: 'gpt-5.2' -}; +}; \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index b65f65b..3ded1e6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,7 @@ import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider } from './contexts/AuthContext'; import { TaskMasterProvider } from './contexts/TaskMasterContext'; import { TasksSettingsProvider } from './contexts/TasksSettingsContext'; -import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext'; +import { WebSocketProvider, useWebSocket } from './contexts/WebSocketContext'; import ProtectedRoute from './components/ProtectedRoute'; import { useVersionCheck } from './hooks/useVersionCheck'; import useLocalStorage from './hooks/useLocalStorage'; @@ -40,11 +40,15 @@ import { I18nextProvider, useTranslation } from 'react-i18next'; import i18n from './i18n/config.js'; +// ! Move to a separate file called AppContent.ts // Main App component with routing function AppContent() { const navigate = useNavigate(); const { sessionId } = useParams(); const { t } = useTranslation('common'); + // * This is a tracker for avoiding excessive re-renders during development + const renderCountRef = useRef(0); + // console.log(`AppContent render count: ${renderCountRef.current++}`); const { updateAvailable, latestVersion, currentVersion, releaseInfo } = useVersionCheck('siteboon', 'claudecodeui'); const [showVersionModal, setShowVersionModal] = useState(false); @@ -81,7 +85,7 @@ function AppContent() { // Triggers ChatInterface to reload messages without switching sessions const [externalMessageUpdate, setExternalMessageUpdate] = useState(0); - const { ws, sendMessage, messages } = useWebSocketContext(); + const { ws, sendMessage, latestMessage } = useWebSocket(); // Ref to track loading progress timeout for cleanup const loadingProgressTimeoutRef = useRef(null); @@ -175,9 +179,7 @@ function AppContent() { // Handle WebSocket messages for real-time project updates useEffect(() => { - if (messages.length > 0) { - const latestMessage = messages[messages.length - 1]; - + if (latestMessage) { // Handle loading progress updates if (latestMessage.type === 'loading_progress') { if (loadingProgressTimeoutRef.current) { @@ -277,7 +279,7 @@ function AppContent() { loadingProgressTimeoutRef.current = null; } }; - }, [messages, selectedProject, selectedSession, activeSessions]); + }, [latestMessage, selectedProject, selectedSession, activeSessions]); const fetchProjects = async () => { try { @@ -916,7 +918,7 @@ function AppContent() { setActiveTab={setActiveTab} ws={ws} sendMessage={sendMessage} - messages={messages} + latestMessage={latestMessage} isMobile={isMobile} isPWA={isPWA} onMenuClick={() => setSidebarOpen(true)} diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 3ee5a84..d3d7bea 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -43,6 +43,8 @@ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelCo import { safeJsonParse } from '../lib/utils.js'; +// ! Move all utility functions to utils/chatUtils.ts + // Helper function to decode HTML entities in text function decodeHtmlEntities(text) { if (!text) return text; @@ -1860,7 +1862,7 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => { // - onReplaceTemporarySession: Called to replace temporary session ID with real WebSocket session ID // // This ensures uninterrupted chat experience by pausing sidebar refreshes during conversations. -function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onSessionProcessing, onSessionNotProcessing, processingSessions, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, onTaskClick, onShowAllTasks }) { +function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, latestMessage, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onSessionProcessing, onSessionNotProcessing, processingSessions, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, onTaskClick, onShowAllTasks }) { const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings(); const { t } = useTranslation('chat'); const [input, setInput] = useState(() => { @@ -3242,8 +3244,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess useEffect(() => { // Handle WebSocket messages - if (messages.length > 0) { - const latestMessage = messages[messages.length - 1]; + if (latestMessage) { const messageData = latestMessage.data?.message || latestMessage.data; // Filter messages by session ID to prevent cross-session interference @@ -4068,7 +4069,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } - }, [messages]); + }, [latestMessage]); // Load file list when project changes useEffect(() => { @@ -4879,7 +4880,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; - +// ! Unused const handleNewSession = () => { setChatMessages([]); setInput(''); diff --git a/src/components/GitPanel.jsx b/src/components/GitPanel.jsx index 3abd4c5..b077b05 100644 --- a/src/components/GitPanel.jsx +++ b/src/components/GitPanel.jsx @@ -960,6 +960,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) { {gitStatus.details && (
{gitStatus.details}
)} + {/* // ! This can be a custom component that can be reused for " Tip: Create a new project..." as well */}
Tip: Run git init in your project directory to initialize git source control.
diff --git a/src/components/LoginModal.jsx b/src/components/LoginModal.jsx
index 6861313..811daf2 100644
--- a/src/components/LoginModal.jsx
+++ b/src/components/LoginModal.jsx
@@ -1,5 +1,6 @@
import { X } from 'lucide-react';
import StandaloneShell from './StandaloneShell';
+import { IS_PLATFORM } from '../constants/config';
/**
* Reusable login modal component for Claude, Cursor, and Codex CLI authentication
@@ -27,15 +28,13 @@ function LoginModal({
const getCommand = () => {
if (customCommand) return customCommand;
- const isPlatform = import.meta.env.VITE_IS_PLATFORM === 'true';
-
switch (provider) {
case 'claude':
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /exit --dangerously-skip-permissions';
case 'cursor':
return 'cursor-agent login';
case 'codex':
- return isPlatform ? 'codex login --device-auth' : 'codex login';
+ return IS_PLATFORM ? 'codex login --device-auth' : 'codex login';
default:
return isAuthenticated ? 'claude setup-token --dangerously-skip-permissions' : 'claude /exit --dangerously-skip-permissions';
}
diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx
index 50949a0..c1db1b5 100644
--- a/src/components/MainContent.jsx
+++ b/src/components/MainContent.jsx
@@ -36,9 +36,9 @@ function MainContent({
setActiveTab,
ws,
sendMessage,
- messages,
+ latestMessage,
isMobile,
- isPWA,
+ isPWA, // ! Unused
onMenuClick,
isLoading,
onInputFocusChange,
@@ -477,7 +477,7 @@ function MainContent({
selectedSession={selectedSession}
ws={ws}
sendMessage={sendMessage}
- messages={messages}
+ latestMessage={latestMessage}
onFileOpen={handleFileOpen}
onInputFocusChange={onInputFocusChange}
onSessionActive={onSessionActive}
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
index 29bf762..17125ae 100644
--- a/src/components/Onboarding.jsx
+++ b/src/components/Onboarding.jsx
@@ -6,6 +6,7 @@ import CodexLogo from './CodexLogo';
import LoginModal from './LoginModal';
import { authenticatedFetch } from '../utils/api';
import { useAuth } from '../contexts/AuthContext';
+import { IS_PLATFORM } from '../constants/config';
const Onboarding = ({ onComplete }) => {
const [currentStep, setCurrentStep] = useState(0);
@@ -15,8 +16,7 @@ const Onboarding = ({ onComplete }) => {
const [error, setError] = useState('');
const [activeLoginProvider, setActiveLoginProvider] = useState(null);
- const isPlatform = import.meta.env.VITE_IS_PLATFORM === 'true';
- const [selectedProject] = useState({ name: 'default', fullPath: isPlatform ? '/workspace' : '' });
+ const [selectedProject] = useState({ name: 'default', fullPath: IS_PLATFORM ? '/workspace' : '' });
const [claudeAuthStatus, setClaudeAuthStatus] = useState({
authenticated: false,
diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx
index 56343ae..507efa8 100644
--- a/src/components/ProtectedRoute.jsx
+++ b/src/components/ProtectedRoute.jsx
@@ -4,6 +4,7 @@ import SetupForm from './SetupForm';
import LoginForm from './LoginForm';
import Onboarding from './Onboarding';
import { MessageSquare } from 'lucide-react';
+import { IS_PLATFORM } from '../constants/config';
const LoadingScreen = () => (