mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-07-09 07:25:43 +08: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 { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||||
import { IS_PLATFORM } from './constants/config.js';
|
import { IS_PLATFORM } from './constants/config.js';
|
||||||
|
|
||||||
// File system watcher for projects folder
|
// File system watchers for provider project/session folders
|
||||||
let projectsWatcher = null;
|
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();
|
const connectedClients = new Set();
|
||||||
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
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() {
|
async function setupProjectsWatcher() {
|
||||||
const chokidar = (await import('chokidar')).default;
|
const chokidar = (await import('chokidar')).default;
|
||||||
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
|
|
||||||
|
|
||||||
if (projectsWatcher) {
|
if (projectsWatcherDebounceTimer) {
|
||||||
projectsWatcher.close();
|
clearTimeout(projectsWatcherDebounceTimer);
|
||||||
|
projectsWatcherDebounceTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await Promise.all(
|
||||||
// Initialize chokidar watcher with optimized settings
|
projectsWatchers.map(async (watcher) => {
|
||||||
projectsWatcher = chokidar.watch(claudeProjectsPath, {
|
try {
|
||||||
ignored: [
|
await watcher.close();
|
||||||
'**/node_modules/**',
|
} catch (error) {
|
||||||
'**/.git/**',
|
console.error('[WARN] Failed to close watcher:', error);
|
||||||
'**/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
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
projectsWatchers = [];
|
||||||
|
|
||||||
// Debounce function to prevent excessive notifications
|
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
||||||
let debounceTimer;
|
if (projectsWatcherDebounceTimer) {
|
||||||
const debouncedUpdate = async (eventType, filePath) => {
|
clearTimeout(projectsWatcherDebounceTimer);
|
||||||
clearTimeout(debounceTimer);
|
}
|
||||||
debounceTimer = setTimeout(async () => {
|
|
||||||
// Prevent reentrant calls
|
projectsWatcherDebounceTimer = setTimeout(async () => {
|
||||||
if (isGetProjectsRunning) {
|
// Prevent reentrant calls
|
||||||
return;
|
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) {
|
// Set up event listeners
|
||||||
console.error('[ERROR] Failed to setup projects watcher:', error);
|
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