mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-15 05:07:35 +00:00
refactor(project-watcher): add codex and cursor file watchers
This commit is contained in:
188
server/index.js
188
server/index.js
@@ -63,8 +63,24 @@ 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;
|
||||
// File system watchers for provider project/session folders
|
||||
const PROVIDER_WATCH_PATHS = [
|
||||
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
||||
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
||||
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
|
||||
];
|
||||
const WATCHER_IGNORED_PATTERNS = [
|
||||
'**/node_modules/**',
|
||||
'**/.git/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/*.tmp',
|
||||
'**/*.swp',
|
||||
'**/.DS_Store'
|
||||
];
|
||||
const WATCHER_DEBOUNCE_MS = 300;
|
||||
let projectsWatchers = [];
|
||||
let projectsWatcherDebounceTimer = null;
|
||||
const connectedClients = new Set();
|
||||
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
||||
|
||||
@@ -81,94 +97,106 @@ function broadcastProgress(progress) {
|
||||
});
|
||||
}
|
||||
|
||||
// Setup file system watcher for Claude projects folder using chokidar
|
||||
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
||||
async function setupProjectsWatcher() {
|
||||
const chokidar = (await import('chokidar')).default;
|
||||
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
|
||||
|
||||
if (projectsWatcher) {
|
||||
projectsWatcher.close();
|
||||
if (projectsWatcherDebounceTimer) {
|
||||
clearTimeout(projectsWatcherDebounceTimer);
|
||||
projectsWatcherDebounceTimer = null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize chokidar watcher with optimized settings
|
||||
projectsWatcher = chokidar.watch(claudeProjectsPath, {
|
||||
ignored: [
|
||||
'**/node_modules/**',
|
||||
'**/.git/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/*.tmp',
|
||||
'**/*.swp',
|
||||
'**/.DS_Store'
|
||||
],
|
||||
persistent: true,
|
||||
ignoreInitial: true, // Don't fire events for existing files on startup
|
||||
followSymlinks: false,
|
||||
depth: 10, // Reasonable depth limit
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
||||
pollInterval: 50
|
||||
await Promise.all(
|
||||
projectsWatchers.map(async (watcher) => {
|
||||
try {
|
||||
await watcher.close();
|
||||
} catch (error) {
|
||||
console.error('[WARN] Failed to close watcher:', error);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
projectsWatchers = [];
|
||||
|
||||
// Debounce function to prevent excessive notifications
|
||||
let debounceTimer;
|
||||
const debouncedUpdate = async (eventType, filePath) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(async () => {
|
||||
// Prevent reentrant calls
|
||||
if (isGetProjectsRunning) {
|
||||
return;
|
||||
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
||||
if (projectsWatcherDebounceTimer) {
|
||||
clearTimeout(projectsWatcherDebounceTimer);
|
||||
}
|
||||
|
||||
projectsWatcherDebounceTimer = setTimeout(async () => {
|
||||
// Prevent reentrant calls
|
||||
if (isGetProjectsRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isGetProjectsRunning = true;
|
||||
|
||||
// Clear project directory cache when files change
|
||||
clearProjectDirectoryCache();
|
||||
|
||||
// Get updated projects list
|
||||
const updatedProjects = await getProjects(broadcastProgress);
|
||||
|
||||
// Notify all connected clients about the project changes
|
||||
const updateMessage = JSON.stringify({
|
||||
type: 'projects_updated',
|
||||
projects: updatedProjects,
|
||||
timestamp: new Date().toISOString(),
|
||||
changeType: eventType,
|
||||
changedFile: path.relative(rootPath, filePath),
|
||||
watchProvider: provider
|
||||
});
|
||||
|
||||
connectedClients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(updateMessage);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Error handling project changes:', error);
|
||||
} finally {
|
||||
isGetProjectsRunning = false;
|
||||
}
|
||||
}, WATCHER_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
||||
try {
|
||||
// Initialize chokidar watcher with optimized settings
|
||||
const watcher = chokidar.watch(rootPath, {
|
||||
ignored: WATCHER_IGNORED_PATTERNS,
|
||||
persistent: true,
|
||||
ignoreInitial: true, // Don't fire events for existing files on startup
|
||||
followSymlinks: false,
|
||||
depth: 10, // Reasonable depth limit
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
||||
pollInterval: 50
|
||||
}
|
||||
|
||||
try {
|
||||
isGetProjectsRunning = true;
|
||||
|
||||
// Clear project directory cache when files change
|
||||
clearProjectDirectoryCache();
|
||||
|
||||
// Get updated projects list
|
||||
const updatedProjects = await getProjects(broadcastProgress);
|
||||
|
||||
// Notify all connected clients about the project changes
|
||||
const updateMessage = JSON.stringify({
|
||||
type: 'projects_updated',
|
||||
projects: updatedProjects,
|
||||
timestamp: new Date().toISOString(),
|
||||
changeType: eventType,
|
||||
changedFile: path.relative(claudeProjectsPath, filePath)
|
||||
});
|
||||
|
||||
connectedClients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(updateMessage);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Error handling project changes:', error);
|
||||
} finally {
|
||||
isGetProjectsRunning = false;
|
||||
}
|
||||
}, 300); // 300ms debounce (slightly faster than before)
|
||||
};
|
||||
|
||||
// Set up event listeners
|
||||
projectsWatcher
|
||||
.on('add', (filePath) => debouncedUpdate('add', filePath))
|
||||
.on('change', (filePath) => debouncedUpdate('change', filePath))
|
||||
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath))
|
||||
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
|
||||
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
|
||||
.on('error', (error) => {
|
||||
console.error('[ERROR] Chokidar watcher error:', error);
|
||||
})
|
||||
.on('ready', () => {
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to setup projects watcher:', error);
|
||||
// Set up event listeners
|
||||
watcher
|
||||
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
||||
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
||||
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
||||
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
||||
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
||||
.on('error', (error) => {
|
||||
console.error(`[ERROR] ${provider} watcher error:`, error);
|
||||
})
|
||||
.on('ready', () => {
|
||||
});
|
||||
|
||||
projectsWatchers.push(watcher);
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (projectsWatchers.length === 0) {
|
||||
console.error('[ERROR] Failed to setup any provider watchers');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user