mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-10 14:59:46 +00:00
Merge pull request #55 from krzemienski/fix/react-errors-and-localStorage-quota
fix: resolve React errors and localStorage quota issues
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-ui",
|
"name": "claude-code-ui",
|
||||||
"version": "1.2.0",
|
"version": "1.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "claude-code-ui",
|
"name": "claude-code-ui",
|
||||||
"version": "1.2.0",
|
"version": "1.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-code": "^1.0.24",
|
"@anthropic-ai/claude-code": "^1.0.24",
|
||||||
|
|||||||
@@ -26,6 +26,88 @@ import ClaudeStatus from './ClaudeStatus';
|
|||||||
import { MicButton } from './MicButton.jsx';
|
import { MicButton } from './MicButton.jsx';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
|
||||||
|
// Safe localStorage utility to handle quota exceeded errors
|
||||||
|
const safeLocalStorage = {
|
||||||
|
setItem: (key, value) => {
|
||||||
|
try {
|
||||||
|
// For chat messages, implement compression and size limits
|
||||||
|
if (key.startsWith('chat_messages_') && typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
// Limit to last 50 messages to prevent storage bloat
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 50) {
|
||||||
|
console.warn(`Truncating chat history for ${key} from ${parsed.length} to 50 messages`);
|
||||||
|
const truncated = parsed.slice(-50);
|
||||||
|
value = JSON.stringify(truncated);
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('Could not parse chat messages for truncation:', parseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'QuotaExceededError') {
|
||||||
|
console.warn('localStorage quota exceeded, clearing old data');
|
||||||
|
// Clear old chat messages to free up space
|
||||||
|
const keys = Object.keys(localStorage);
|
||||||
|
const chatKeys = keys.filter(k => k.startsWith('chat_messages_')).sort();
|
||||||
|
|
||||||
|
// Remove oldest chat data first, keeping only the 3 most recent projects
|
||||||
|
if (chatKeys.length > 3) {
|
||||||
|
chatKeys.slice(0, chatKeys.length - 3).forEach(k => {
|
||||||
|
localStorage.removeItem(k);
|
||||||
|
console.log(`Removed old chat data: ${k}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still failing, clear draft inputs too
|
||||||
|
const draftKeys = keys.filter(k => k.startsWith('draft_input_'));
|
||||||
|
draftKeys.forEach(k => {
|
||||||
|
localStorage.removeItem(k);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try again with reduced data
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
} catch (retryError) {
|
||||||
|
console.error('Failed to save to localStorage even after cleanup:', retryError);
|
||||||
|
// Last resort: Try to save just the last 10 messages
|
||||||
|
if (key.startsWith('chat_messages_') && typeof value === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 10) {
|
||||||
|
const minimal = parsed.slice(-10);
|
||||||
|
localStorage.setItem(key, JSON.stringify(minimal));
|
||||||
|
console.warn('Saved only last 10 messages due to quota constraints');
|
||||||
|
}
|
||||||
|
} catch (finalError) {
|
||||||
|
console.error('Final save attempt failed:', finalError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('localStorage error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getItem: (key) => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('localStorage getItem error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeItem: (key) => {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('localStorage removeItem error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Memoized message component to prevent unnecessary re-renders
|
// Memoized message component to prevent unnecessary re-renders
|
||||||
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
|
const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => {
|
||||||
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
const isGrouped = prevMessage && prevMessage.type === message.type &&
|
||||||
@@ -1019,13 +1101,13 @@ const ImageAttachment = ({ file, onRemove, uploadProgress, error }) => {
|
|||||||
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
|
function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, messages, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, autoScrollToBottom, sendByCtrlEnter }) {
|
||||||
const [input, setInput] = useState(() => {
|
const [input, setInput] = useState(() => {
|
||||||
if (typeof window !== 'undefined' && selectedProject) {
|
if (typeof window !== 'undefined' && selectedProject) {
|
||||||
return localStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
return safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
const [chatMessages, setChatMessages] = useState(() => {
|
const [chatMessages, setChatMessages] = useState(() => {
|
||||||
if (typeof window !== 'undefined' && selectedProject) {
|
if (typeof window !== 'undefined' && selectedProject) {
|
||||||
const saved = localStorage.getItem(`chat_messages_${selectedProject.name}`);
|
const saved = safeLocalStorage.getItem(`chat_messages_${selectedProject.name}`);
|
||||||
return saved ? JSON.parse(saved) : [];
|
return saved ? JSON.parse(saved) : [];
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
@@ -1310,16 +1392,16 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Persist input draft to localStorage
|
// Persist input draft to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProject && input !== '') {
|
if (selectedProject && input !== '') {
|
||||||
localStorage.setItem(`draft_input_${selectedProject.name}`, input);
|
safeLocalStorage.setItem(`draft_input_${selectedProject.name}`, input);
|
||||||
} else if (selectedProject && input === '') {
|
} else if (selectedProject && input === '') {
|
||||||
localStorage.removeItem(`draft_input_${selectedProject.name}`);
|
safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`);
|
||||||
}
|
}
|
||||||
}, [input, selectedProject]);
|
}, [input, selectedProject]);
|
||||||
|
|
||||||
// Persist chat messages to localStorage
|
// Persist chat messages to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProject && chatMessages.length > 0) {
|
if (selectedProject && chatMessages.length > 0) {
|
||||||
localStorage.setItem(`chat_messages_${selectedProject.name}`, JSON.stringify(chatMessages));
|
safeLocalStorage.setItem(`chat_messages_${selectedProject.name}`, JSON.stringify(chatMessages));
|
||||||
}
|
}
|
||||||
}, [chatMessages, selectedProject]);
|
}, [chatMessages, selectedProject]);
|
||||||
|
|
||||||
@@ -1327,7 +1409,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
// Always load saved input draft for the project
|
// Always load saved input draft for the project
|
||||||
const savedInput = localStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
const savedInput = safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
|
||||||
if (savedInput !== input) {
|
if (savedInput !== input) {
|
||||||
setInput(savedInput);
|
setInput(savedInput);
|
||||||
}
|
}
|
||||||
@@ -1546,7 +1628,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
// Clear persisted chat messages after successful completion
|
// Clear persisted chat messages after successful completion
|
||||||
if (selectedProject && latestMessage.exitCode === 0) {
|
if (selectedProject && latestMessage.exitCode === 0) {
|
||||||
localStorage.removeItem(`chat_messages_${selectedProject.name}`);
|
safeLocalStorage.removeItem(`chat_messages_${selectedProject.name}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -1803,14 +1885,33 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Handle image files from drag & drop or file picker
|
// Handle image files from drag & drop or file picker
|
||||||
const handleImageFiles = useCallback((files) => {
|
const handleImageFiles = useCallback((files) => {
|
||||||
const validFiles = files.filter(file => {
|
const validFiles = files.filter(file => {
|
||||||
if (!file.type.startsWith('image/')) {
|
try {
|
||||||
|
// Validate file object and properties
|
||||||
|
if (!file || typeof file !== 'object') {
|
||||||
|
console.warn('Invalid file object:', file);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
setImageErrors(prev => new Map(prev).set(file.name, 'File too large (max 5MB)'));
|
if (!file.type || !file.type.startsWith('image/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!file.size || file.size > 5 * 1024 * 1024) {
|
||||||
|
// Safely get file name with fallback
|
||||||
|
const fileName = file.name || 'Unknown file';
|
||||||
|
setImageErrors(prev => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
newMap.set(fileName, 'File too large (max 5MB)');
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error validating file:', error, file);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
if (validFiles.length > 0) {
|
||||||
@@ -1866,7 +1967,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth-token');
|
const token = safeLocalStorage.getItem('auth-token');
|
||||||
const headers = {};
|
const headers = {};
|
||||||
if (token) {
|
if (token) {
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
@@ -1929,7 +2030,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
// Get tools settings from localStorage
|
// Get tools settings from localStorage
|
||||||
const getToolsSettings = () => {
|
const getToolsSettings = () => {
|
||||||
try {
|
try {
|
||||||
const savedSettings = localStorage.getItem('claude-tools-settings');
|
const savedSettings = safeLocalStorage.getItem('claude-tools-settings');
|
||||||
if (savedSettings) {
|
if (savedSettings) {
|
||||||
return JSON.parse(savedSettings);
|
return JSON.parse(savedSettings);
|
||||||
}
|
}
|
||||||
@@ -1975,7 +2076,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
|
|||||||
|
|
||||||
// Clear the saved draft since message was sent
|
// Clear the saved draft since message was sent
|
||||||
if (selectedProject) {
|
if (selectedProject) {
|
||||||
localStorage.removeItem(`draft_input_${selectedProject.name}`);
|
safeLocalStorage.removeItem(`draft_input_${selectedProject.name}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
73
src/components/ErrorBoundary.jsx
Normal file
73
src/components/ErrorBoundary.jsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class ErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null, errorInfo: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
// Update state so the next render will show the fallback UI
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
// Log the error details
|
||||||
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||||
|
|
||||||
|
// You can also log the error to an error reporting service here
|
||||||
|
this.setState({
|
||||||
|
error: error,
|
||||||
|
errorInfo: errorInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Fallback UI
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="ml-3 text-sm font-medium text-red-800">
|
||||||
|
Something went wrong
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-red-700">
|
||||||
|
<p className="mb-2">An error occurred while loading the chat interface.</p>
|
||||||
|
{this.props.showDetails && this.state.error && (
|
||||||
|
<details className="mt-4">
|
||||||
|
<summary className="cursor-pointer text-xs font-mono">Error Details</summary>
|
||||||
|
<pre className="mt-2 text-xs bg-red-100 p-2 rounded overflow-auto max-h-40">
|
||||||
|
{this.state.error.toString()}
|
||||||
|
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({ hasError: false, error: null, errorInfo: null });
|
||||||
|
if (this.props.onRetry) this.props.onRetry();
|
||||||
|
}}
|
||||||
|
className="bg-red-600 text-white px-4 py-2 rounded text-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -17,6 +17,7 @@ import FileTree from './FileTree';
|
|||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from './CodeEditor';
|
||||||
import Shell from './Shell';
|
import Shell from './Shell';
|
||||||
import GitPanel from './GitPanel';
|
import GitPanel from './GitPanel';
|
||||||
|
import ErrorBoundary from './ErrorBoundary';
|
||||||
|
|
||||||
function MainContent({
|
function MainContent({
|
||||||
selectedProject,
|
selectedProject,
|
||||||
@@ -270,6 +271,7 @@ function MainContent({
|
|||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||||
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
<div className={`h-full ${activeTab === 'chat' ? 'block' : 'hidden'}`}>
|
||||||
|
<ErrorBoundary showDetails={true}>
|
||||||
<ChatInterface
|
<ChatInterface
|
||||||
selectedProject={selectedProject}
|
selectedProject={selectedProject}
|
||||||
selectedSession={selectedSession}
|
selectedSession={selectedSession}
|
||||||
@@ -288,6 +290,7 @@ function MainContent({
|
|||||||
autoScrollToBottom={autoScrollToBottom}
|
autoScrollToBottom={autoScrollToBottom}
|
||||||
sendByCtrlEnter={sendByCtrlEnter}
|
sendByCtrlEnter={sendByCtrlEnter}
|
||||||
/>
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>
|
<div className={`h-full overflow-hidden ${activeTab === 'files' ? 'block' : 'hidden'}`}>
|
||||||
<FileTree selectedProject={selectedProject} />
|
<FileTree selectedProject={selectedProject} />
|
||||||
|
|||||||
Reference in New Issue
Block a user