mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-14 20:57:32 +00:00
Refactor WebSocket context + centralize platform flag (#363)
* fix: remove unnecessary websocket.js file and replace its usage directly in `WebSocketContext` * fix: connect() doesn't need to be async * fix: update WebSocket context import to use useWebSocket hook * fix: use `useRef` for WebSocketContext The main issue with using states was, previously the websocket never closed properly on unmount, so multiple connections could be opened. This was because the useEffect cleanup function was closing an old websocket (that was initialized to null) instead of the current one. We could have fixed this by adding `ws` to the useEffect dependency array, but this was unnecessary since `ws` doesn't affect rendering so we shouldn't use a state. * fix: replace `WebSocketContext` default value with null and add type definitions * fix: add type definition for WebSocket URL and remove redundant protocol declaration * fix: Prevent WebSocket reconnection attempts after unmount Right now, when the WebSocketContext component unmounts, there is still a pending reconnection attempt that tries to reconnect the WebSocket after 3 seconds. * refactor: Extract WebSocket URL construction into a separate function * refactor: Centralize platform mode detection using IS_PLATFORM constant; use `token` from Auth context in WebSocket connection * refactor: Use IS_PLATFORM constant for platform detection in authenticatedFetch function (backend) * refactor: move IS_PLATFORM to config file for both frontend and backend The reason we couldn't place it in shared/modelConstants.js is that the frontend uses Vite which requires import.meta.env for environment variables, while the backend uses process.env. Therefore, we created separate config files for the frontend (src/constants/config.ts) and backend (server/constants/config.js). * refactor: update import path for IS_PLATFORM constant to use config file * refactor: replace `messages` with `latestMessage` in WebSocket context and related components Why? Because, messages was only being used to access the latest message in the components it's used in. * refactor: optimize WebSocket connection handling with useCallback and useMemo * refactor: comment out debug log for render count in AppContent component * refactor(backend): update environment variable handling and replace VITE_IS_PLATFORM with IS_PLATFORM constant * refactor: update WebSocket connection effect to depend on token changes for reconnection ---------
This commit is contained in:
@@ -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('');
|
||||
|
||||
@@ -960,6 +960,7 @@ function GitPanel({ selectedProject, isMobile, onFileOpen }) {
|
||||
{gitStatus.details && (
|
||||
<p className="text-sm text-center leading-relaxed mb-6 max-w-md">{gitStatus.details}</p>
|
||||
)}
|
||||
{/* // ! This can be a custom component that can be reused for " Tip: Create a new project..." as well */}
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 max-w-md">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 text-center">
|
||||
<strong>Tip:</strong> Run <code className="bg-blue-100 dark:bg-blue-900 px-2 py-1 rounded font-mono text-xs">git init</code> in your project directory to initialize git source control.
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = () => (
|
||||
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
@@ -27,7 +28,7 @@ const LoadingScreen = () => (
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { user, isLoading, needsSetup, hasCompletedOnboarding, refreshOnboardingStatus } = useAuth();
|
||||
|
||||
if (import.meta.env.VITE_IS_PLATFORM === 'true') {
|
||||
if (IS_PLATFORM) {
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IS_PLATFORM } from '../constants/config';
|
||||
|
||||
const xtermStyles = `
|
||||
.xterm .xterm-screen {
|
||||
@@ -55,10 +56,9 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
||||
if (isConnecting || isConnected) return;
|
||||
|
||||
try {
|
||||
const isPlatform = import.meta.env.VITE_IS_PLATFORM === 'true';
|
||||
let wsUrl;
|
||||
|
||||
if (isPlatform) {
|
||||
if (IS_PLATFORM) {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
wsUrl = `${protocol}//${window.location.host}/shell`;
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ import ProjectCreationWizard from './ProjectCreationWizard';
|
||||
import { api } from '../utils/api';
|
||||
import { useTaskMaster } from '../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../contexts/TasksSettingsContext';
|
||||
import { IS_PLATFORM } from '../constants/config';
|
||||
|
||||
// Move formatTimeAgo outside component to avoid recreation on every render
|
||||
const formatTimeAgo = (dateString, currentTime, t) => {
|
||||
@@ -622,7 +623,7 @@ function Sidebar({
|
||||
<div className="md:p-4 md:border-b md:border-border">
|
||||
{/* Desktop Header */}
|
||||
<div className="hidden md:flex items-center justify-between">
|
||||
{import.meta.env.VITE_IS_PLATFORM === 'true' ? (
|
||||
{IS_PLATFORM ? (
|
||||
<a
|
||||
href="https://cloudcli.ai/dashboard"
|
||||
className="flex items-center gap-3 hover:opacity-80 transition-opacity group"
|
||||
@@ -673,7 +674,7 @@ function Sidebar({
|
||||
style={isPWA && isMobile ? { paddingTop: '16px' } : {}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{import.meta.env.VITE_IS_PLATFORM === 'true' ? (
|
||||
{IS_PLATFORM ? (
|
||||
<a
|
||||
href="https://cloudcli.ai/dashboard"
|
||||
className="flex items-center gap-3 active:opacity-70 transition-opacity"
|
||||
|
||||
Reference in New Issue
Block a user