From d2f02558a121df74d422da5cd75c45ab46b42439 Mon Sep 17 00:00:00 2001 From: simos Date: Fri, 31 Oct 2025 00:37:20 +0000 Subject: [PATCH 1/3] feat(editor): Change Code Editor to show diffs in source control panel and during messaging. Add merge view and minimap extensions to CodeMirror for enhanced code editing capabilities. Increase Express JSON and URL-encoded payload limits from default (100kb) to 50mb to support larger file operations and git diffs. --- package-lock.json | 32 +++ package.json | 8 +- server/index.js | 22 +- server/routes/git.js | 105 +++++++- src/App.jsx | 4 - src/components/ChatInterface.jsx | 200 +++++++++++---- src/components/CodeEditor.jsx | 411 +++++++++++++++++++++---------- src/components/GitPanel.jsx | 64 ++++- src/components/MainContent.jsx | 2 +- 9 files changed, 644 insertions(+), 204 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8c3264d..993b268 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-markdown": "^6.3.3", "@codemirror/lang-python": "^6.2.1", + "@codemirror/merge": "^6.11.1", "@codemirror/theme-one-dark": "^6.1.2", + "@replit/codemirror-minimap": "^0.5.2", "@tailwindcss/typography": "^0.5.16", "@uiw/react-codemirror": "^4.23.13", "@xterm/addon-clipboard": "^0.1.0", @@ -536,6 +538,19 @@ "crelt": "^1.0.5" } }, + "node_modules/@codemirror/merge": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/merge/-/merge-6.11.1.tgz", + "integrity": "sha512-NleJ//mSmcal3jRdm9WwOVMUaJWvP2h69K96z3xTDJnde/nsMnLt9qfKUBkycWm5iO3/g4Zd69XTuTFErTZ72A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/highlight": "^1.0.0", + "style-mod": "^4.1.0" + } + }, "node_modules/@codemirror/search": { "version": "6.5.11", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", @@ -2450,6 +2465,23 @@ "node": ">=14.0.0" } }, + "node_modules/@replit/codemirror-minimap": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-minimap/-/codemirror-minimap-0.5.2.tgz", + "integrity": "sha512-eNAtpr0hOG09/5zqAQ5PkgZEb3V/MHi30zentCxiR73r+utR2m9yVMCpBmfsWbb8mWxUWhMGPiHxM5hFtnscQA==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.5" + }, + "peerDependencies": { + "@codemirror/language": "^6.9.1", + "@codemirror/lint": "^6.4.2", + "@codemirror/state": "^6.3.1", + "@codemirror/view": "^6.21.3", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.1.6" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", diff --git a/package.json b/package.json index 24c3f70..acfbe3a 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,15 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-markdown": "^6.3.3", "@codemirror/lang-python": "^6.2.1", + "@codemirror/merge": "^6.11.1", "@codemirror/theme-one-dark": "^6.1.2", + "@replit/codemirror-minimap": "^0.5.2", "@tailwindcss/typography": "^0.5.16", "@uiw/react-codemirror": "^4.23.13", "@xterm/addon-clipboard": "^0.1.0", + "@xterm/addon-fit": "^0.10.0", "@xterm/addon-webgl": "^0.18.0", + "@xterm/xterm": "^5.5.0", "bcrypt": "^6.0.0", "better-sqlite3": "^12.2.0", "chokidar": "^4.0.3", @@ -75,9 +79,7 @@ "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "tailwind-merge": "^3.3.1", - "ws": "^8.14.2", - "@xterm/xterm": "^5.5.0", - "@xterm/addon-fit": "^0.10.0" + "ws": "^8.14.2" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/server/index.js b/server/index.js index dfe7c81..8ab26d7 100755 --- a/server/index.js +++ b/server/index.js @@ -172,7 +172,8 @@ const wss = new WebSocketServer({ app.locals.wss = wss; app.use(cors()); -app.use(express.json()); +app.use(express.json({ limit: '50mb' })); +app.use(express.urlencoded({ limit: '50mb', extended: true })); // Optional API key validation (if configured) app.use('/api', validateApiKey); @@ -408,7 +409,10 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) = return res.status(404).json({ error: 'Project not found' }); } - const resolved = path.resolve(filePath); + // Handle both absolute and relative paths + const resolved = path.isAbsolute(filePath) + ? path.resolve(filePath) + : path.resolve(projectRoot, filePath); const normalizedRoot = path.resolve(projectRoot) + path.sep; if (!resolved.startsWith(normalizedRoot)) { return res.status(403).json({ error: 'Path must be under project root' }); @@ -504,21 +508,15 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) = return res.status(404).json({ error: 'Project not found' }); } - const resolved = path.resolve(filePath); + // Handle both absolute and relative paths + const resolved = path.isAbsolute(filePath) + ? path.resolve(filePath) + : path.resolve(projectRoot, filePath); const normalizedRoot = path.resolve(projectRoot) + path.sep; if (!resolved.startsWith(normalizedRoot)) { return res.status(403).json({ error: 'Path must be under project root' }); } - // Create backup of original file - try { - const backupPath = resolved + '.backup.' + Date.now(); - await fsPromises.copyFile(resolved, backupPath); - console.log('πŸ“‹ Created backup:', backupPath); - } catch (backupError) { - console.warn('Could not create backup:', backupError.message); - } - // Write the new content await fsPromises.writeFile(resolved, content, 'utf8'); diff --git a/server/routes/git.js b/server/routes/git.js index 3294c92..da917d0 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -21,6 +21,35 @@ async function getActualProjectPath(projectName) { } } +// Helper function to strip git diff headers +function stripDiffHeaders(diff) { + if (!diff) return ''; + + const lines = diff.split('\n'); + const filteredLines = []; + let startIncluding = false; + + for (const line of lines) { + // Skip all header lines including diff --git, index, file mode, and --- / +++ file paths + if (line.startsWith('diff --git') || + line.startsWith('index ') || + line.startsWith('new file mode') || + line.startsWith('deleted file mode') || + line.startsWith('---') || + line.startsWith('+++')) { + continue; + } + + // Start including lines from @@ hunk headers onwards + if (line.startsWith('@@') || startIncluding) { + startIncluding = true; + filteredLines.push(line); + } + } + + return filteredLines.join('\n'); +} + // Helper function to validate git repository async function validateGitRepository(projectPath) { try { @@ -124,32 +153,39 @@ router.get('/diff', async (req, res) => { // Validate git repository await validateGitRepository(projectPath); - // Check if file is untracked + // Check if file is untracked or deleted const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath }); const isUntracked = statusOutput.startsWith('??'); - + const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D'); + let diff; if (isUntracked) { // For untracked files, show the entire file content as additions const fileContent = await fs.readFile(path.join(projectPath, file), 'utf-8'); const lines = fileContent.split('\n'); - diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` + + diff = `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n` + lines.map(line => `+${line}`).join('\n'); + } else if (isDeleted) { + // For deleted files, show the entire file content from HEAD as deletions + const { stdout: fileContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath }); + const lines = fileContent.split('\n'); + diff = `--- a/${file}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n` + + lines.map(line => `-${line}`).join('\n'); } else { // Get diff for tracked files // First check for unstaged changes (working tree vs index) const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath }); - + if (unstagedDiff) { // Show unstaged changes if they exist - diff = unstagedDiff; + diff = stripDiffHeaders(unstagedDiff); } else { // If no unstaged changes, check for staged changes (index vs HEAD) const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath }); - diff = stagedDiff || ''; + diff = stripDiffHeaders(stagedDiff) || ''; } } - + res.json({ diff }); } catch (error) { console.error('Git diff error:', error); @@ -157,6 +193,61 @@ router.get('/diff', async (req, res) => { } }); +// Get file content with diff information for CodeEditor +router.get('/file-with-diff', async (req, res) => { + const { project, file } = req.query; + + if (!project || !file) { + return res.status(400).json({ error: 'Project name and file path are required' }); + } + + try { + const projectPath = await getActualProjectPath(project); + + // Validate git repository + await validateGitRepository(projectPath); + + // Check file status + const { stdout: statusOutput } = await execAsync(`git status --porcelain "${file}"`, { cwd: projectPath }); + const isUntracked = statusOutput.startsWith('??'); + const isDeleted = statusOutput.trim().startsWith('D ') || statusOutput.trim().startsWith(' D'); + + let currentContent = ''; + let oldContent = ''; + + if (isDeleted) { + // For deleted files, get content from HEAD + const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath }); + oldContent = headContent; + currentContent = headContent; // Show the deleted content in editor + } else { + // Get current file content + currentContent = await fs.readFile(path.join(projectPath, file), 'utf-8'); + + if (!isUntracked) { + // Get the old content from HEAD for tracked files + try { + const { stdout: headContent } = await execAsync(`git show HEAD:"${file}"`, { cwd: projectPath }); + oldContent = headContent; + } catch (error) { + // File might be newly added to git (staged but not committed) + oldContent = ''; + } + } + } + + res.json({ + currentContent, + oldContent, + isDeleted, + isUntracked + }); + } catch (error) { + console.error('Git file-with-diff error:', error); + res.json({ error: error.message }); + } +}); + // Commit changes router.post('/commit', async (req, res) => { const { project, message, files } = req.body; diff --git a/src/App.jsx b/src/App.jsx index fe01254..7d5a982 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -184,11 +184,7 @@ function AppContent() { if (!isSessionActive) { // Session is not active - safe to reload messages - console.log('πŸ”„ External CLI update detected for current session:', changedSessionId); setExternalMessageUpdate(prev => prev + 1); - } else { - // Session is active - skip reload to avoid interrupting user - console.log('⏸️ External update paused - session is active:', changedSessionId); } } } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 0a217d9..d99120d 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -170,7 +170,7 @@ const safeLocalStorage = { }; // Memoized message component to prevent unnecessary re-renders -const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters, showThinking }) => { +const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters, showThinking, selectedProject }) => { const isGrouped = prevMessage && prevMessage.type === message.type && ((prevMessage.type === 'assistant') || (prevMessage.type === 'user') || @@ -315,14 +315,37 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile πŸ“ View edit diff for - + + + `; + + const prevBtn = dom.querySelector('.cm-diff-nav-prev'); + const nextBtn = dom.querySelector('.cm-diff-nav-next'); + + prevBtn?.addEventListener('click', () => { + if (chunks.length === 0) return; + currentIndex = currentIndex > 0 ? currentIndex - 1 : chunks.length - 1; + + // Navigate to the chunk - use fromB which is the position in the current document + const chunk = chunks[currentIndex]; + if (chunk) { + // Scroll to the start of the chunk in the B side (current document) + view.dispatch({ + effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) + }); + } + updatePanel(); + }); + + nextBtn?.addEventListener('click', () => { + if (chunks.length === 0) return; + currentIndex = currentIndex < chunks.length - 1 ? currentIndex + 1 : 0; + + // Navigate to the chunk - use fromB which is the position in the current document + const chunk = chunks[currentIndex]; + if (chunk) { + // Scroll to the start of the chunk in the B side (current document) + view.dispatch({ + effects: EditorView.scrollIntoView(chunk.fromB, { y: 'center' }) + }); + } + updatePanel(); + }); + }; + + updatePanel(); + + return { + top: true, + dom, + update: updatePanel + }; + }; + + return [showPanel.of(createPanel)]; + }, [file.diffInfo, showDiff]); // Get language extension based on file extension const getLanguageExtension = (filename) => { @@ -139,13 +200,24 @@ function CodeEditor({ file, onClose, projectPath }) { const loadFileContent = async () => { try { setLoading(true); - + + // If we have diffInfo with both old and new content, we can show the diff directly + // This handles both GitPanel (full content) and ChatInterface (full content from API) + if (file.diffInfo && file.diffInfo.new_string !== undefined && file.diffInfo.old_string !== undefined) { + // Use the new_string as the content to display + // The unifiedMergeView will compare it against old_string + setContent(file.diffInfo.new_string); + setLoading(false); + return; + } + + // Otherwise, load from disk const response = await api.readFile(file.projectName, file.path); - + if (!response.ok) { throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); } - + const data = await response.json(); setContent(data.content); } catch (error) { @@ -159,37 +231,41 @@ function CodeEditor({ file, onClose, projectPath }) { loadFileContent(); }, [file, projectPath]); - // Update diff decorations when content or diff info changes - const editorRef = useRef(null); - - useEffect(() => { - if (editorRef.current && content && file.diffInfo && showDiff) { - const decorations = createDiffDecorations(content, file.diffInfo); - const view = editorRef.current.view; - if (view) { - view.dispatch({ - effects: diffEffect.of(decorations) - }); - } - } - }, [content, file.diffInfo, showDiff, isDarkMode]); - const handleSave = async () => { setSaving(true); try { + console.log('Saving file:', { + projectName: file.projectName, + path: file.path, + contentLength: content?.length + }); + const response = await api.saveFile(file.projectName, file.path, content); + console.log('Save response:', { + status: response.status, + ok: response.ok, + contentType: response.headers.get('content-type') + }); + if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || `Save failed: ${response.status}`); + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const errorData = await response.json(); + throw new Error(errorData.error || `Save failed: ${response.status}`); + } else { + const textError = await response.text(); + console.error('Non-JSON error response:', textError); + throw new Error(`Save failed: ${response.status} ${response.statusText}`); + } } const result = await response.json(); - - // Show success feedback + console.log('Save successful:', result); + setSaveSuccess(true); - setTimeout(() => setSaveSuccess(false), 2000); // Hide after 2 seconds - + setTimeout(() => setSaveSuccess(false), 2000); + } catch (error) { console.error('Error saving file:', error); alert(`Error saving file: ${error.message}`); @@ -258,11 +334,80 @@ function CodeEditor({ file, onClose, projectPath }) { } return ( -
-
+ +
+
{file.path}

- +
{file.diffInfo && ( )} - + - + - + - + - + - +
- +
Press Ctrl+S to save β€’ Esc to close
+ ); } -export default CodeEditor; \ No newline at end of file +export default CodeEditor; diff --git a/src/components/GitPanel.jsx b/src/components/GitPanel.jsx index a7459f7..5b97604 100644 --- a/src/components/GitPanel.jsx +++ b/src/components/GitPanel.jsx @@ -4,7 +4,7 @@ import { MicButton } from './MicButton.jsx'; import { authenticatedFetch } from '../utils/api'; import DiffViewer from './DiffViewer.jsx'; -function GitPanel({ selectedProject, isMobile }) { +function GitPanel({ selectedProject, isMobile, onFileOpen }) { const [gitStatus, setGitStatus] = useState(null); const [gitDiff, setGitDiff] = useState({}); const [isLoading, setIsLoading] = useState(false); @@ -107,6 +107,12 @@ function GitPanel({ selectedProject, isMobile }) { for (const file of data.added || []) { fetchFileDiff(file); } + for (const file of data.deleted || []) { + fetchFileDiff(file); + } + for (const file of data.untracked || []) { + fetchFileDiff(file); + } } } catch (error) { console.error('Error fetching git status:', error); @@ -402,7 +408,7 @@ function GitPanel({ selectedProject, isMobile }) { try { const response = await authenticatedFetch(`/api/git/diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`); const data = await response.json(); - + if (!data.error && data.diff) { setGitDiff(prev => ({ ...prev, @@ -414,6 +420,36 @@ function GitPanel({ selectedProject, isMobile }) { } }; + const handleFileOpen = async (filePath) => { + if (!onFileOpen) return; + + try { + // Fetch file content with diff information + const response = await authenticatedFetch(`/api/git/file-with-diff?project=${encodeURIComponent(selectedProject.name)}&file=${encodeURIComponent(filePath)}`); + const data = await response.json(); + + if (data.error) { + console.error('Error fetching file with diff:', data.error); + // Fallback: open without diff info + onFileOpen(filePath); + return; + } + + // Create diffInfo object for CodeEditor + const diffInfo = { + old_string: data.oldContent || '', + new_string: data.currentContent || '' + }; + + // Open file with diff information + onFileOpen(filePath, diffInfo); + } catch (error) { + console.error('Error opening file:', error); + // Fallback: open without diff info + onFileOpen(filePath); + } + }; + const fetchRecentCommits = async () => { try { const response = await authenticatedFetch(`/api/git/commits?project=${encodeURIComponent(selectedProject.name)}&limit=10`); @@ -610,14 +646,28 @@ function GitPanel({ selectedProject, isMobile }) { onClick={(e) => e.stopPropagation()} className={`rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-500 focus:ring-blue-500 dark:focus:ring-blue-400 dark:bg-gray-800 dark:checked:bg-blue-600 ${isMobile ? 'mr-1.5' : 'mr-2'}`} /> -
toggleFileExpanded(filePath)} +
-
+
{ + e.stopPropagation(); + toggleFileExpanded(filePath); + }} + >
- {filePath} + { + e.stopPropagation(); + handleFileOpen(filePath); + }} + title="Click to open file" + > + {filePath} +
{(status === 'M' || status === 'D') && (
- +
{shouldShowTasksTab && (
From 1bc2cf49ecc128beee37f58ce2e854dd78df9f14 Mon Sep 17 00:00:00 2001 From: simos Date: Fri, 31 Oct 2025 00:41:06 +0000 Subject: [PATCH 2/3] fix(ui): stabilize token rate calculation in status component Calculate token rate once per timing session instead of recalculating on every interval tick. This prevents the simulated token count from jumping erratically due to random value changes. Also remove noisy console warning in websocket utility that was cluttering development logs without providing actionable information. --- src/components/ClaudeStatus.jsx | 7 +++++-- src/utils/websocket.js | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ClaudeStatus.jsx b/src/components/ClaudeStatus.jsx index 05eaac2..2fb54ad 100644 --- a/src/components/ClaudeStatus.jsx +++ b/src/components/ClaudeStatus.jsx @@ -15,11 +15,14 @@ function ClaudeStatus({ status, onAbort, isLoading, provider = 'claude' }) { } const startTime = Date.now(); + // Calculate random token rate once (30-50 tokens per second) + const tokenRate = 30 + Math.random() * 20; + const timer = setInterval(() => { const elapsed = Math.floor((Date.now() - startTime) / 1000); setElapsedTime(elapsed); - // Simulate token count increasing over time (roughly 30-50 tokens per second) - setFakeTokens(Math.floor(elapsed * (30 + Math.random() * 20))); + // Simulate token count increasing over time + setFakeTokens(Math.floor(elapsed * tokenRate)); }, 1000); return () => clearInterval(timer); diff --git a/src/utils/websocket.js b/src/utils/websocket.js index 6c813ce..3f36e2a 100755 --- a/src/utils/websocket.js +++ b/src/utils/websocket.js @@ -41,7 +41,6 @@ export function useWebSocket() { // If the config returns localhost but we're not on localhost, use current host but with API server port if (wsBaseUrl.includes('localhost') && !window.location.hostname.includes('localhost')) { - console.warn('Config returned localhost, using current host with API server port instead'); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; // For development, API server is typically on port 3002 when Vite is on 3001 const apiPort = window.location.port === '3001' ? '3002' : window.location.port; From 53c1af33fa1cbcc784e43891592e822316ca47b4 Mon Sep 17 00:00:00 2001 From: simos Date: Fri, 31 Oct 2025 00:46:56 +0000 Subject: [PATCH 3/3] fix(App): wrap session handlers in useCallback to avoid warnings on depth Wrap markSessionAsActive, markSessionAsInactive, markSessionAsProcessing, markSessionAsNotProcessing, and replaceTemporarySession functions in useCallback hooks to prevent unnecessary re-renders and stabilize function references across component lifecycle. This optimization ensures child components receiving these callbacks won't re-render unnecessarily when AppContent re-renders. --- src/App.jsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7d5a982..d6aac09 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,7 +18,7 @@ * Handles both existing sessions (with real IDs) and new sessions (with temporary IDs). */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { BrowserRouter as Router, Routes, Route, useNavigate, useParams } from 'react-router-dom'; import Sidebar from './components/Sidebar'; import MainContent from './components/MainContent'; @@ -478,14 +478,14 @@ function AppContent() { // markSessionAsActive: Called when user sends a message to mark session as protected // This includes both real session IDs and temporary "new-session-*" identifiers - const markSessionAsActive = (sessionId) => { + const markSessionAsActive = useCallback((sessionId) => { if (sessionId) { setActiveSessions(prev => new Set([...prev, sessionId])); } - }; + }, []); // markSessionAsInactive: Called when conversation completes/aborts to re-enable project updates - const markSessionAsInactive = (sessionId) => { + const markSessionAsInactive = useCallback((sessionId) => { if (sessionId) { setActiveSessions(prev => { const newSet = new Set(prev); @@ -493,19 +493,19 @@ function AppContent() { return newSet; }); } - }; + }, []); // Processing Session Functions: Track which sessions are currently thinking/processing // markSessionAsProcessing: Called when Claude starts thinking/processing - const markSessionAsProcessing = (sessionId) => { + const markSessionAsProcessing = useCallback((sessionId) => { if (sessionId) { setProcessingSessions(prev => new Set([...prev, sessionId])); } - }; + }, []); // markSessionAsNotProcessing: Called when Claude finishes thinking/processing - const markSessionAsNotProcessing = (sessionId) => { + const markSessionAsNotProcessing = useCallback((sessionId) => { if (sessionId) { setProcessingSessions(prev => { const newSet = new Set(prev); @@ -513,12 +513,12 @@ function AppContent() { return newSet; }); } - }; + }, []); // replaceTemporarySession: Called when WebSocket provides real session ID for new sessions // Removes temporary "new-session-*" identifiers and adds the real session ID // This maintains protection continuity during the transition from temporary to real session - const replaceTemporarySession = (realSessionId) => { + const replaceTemporarySession = useCallback((realSessionId) => { if (realSessionId) { setActiveSessions(prev => { const newSet = new Set(); @@ -532,7 +532,7 @@ function AppContent() { return newSet; }); } - }; + }, []); // Version Upgrade Modal Component const VersionUpgradeModal = () => {