refactor: use updated session watcher

In addition, for projects_updated websocket response, send the sessionId instead
This commit is contained in:
Haileyesus
2026-04-24 21:56:19 +03:00
parent b3445508e9
commit 3fd2353ffe
12 changed files with 97 additions and 209 deletions

View File

@@ -36,7 +36,6 @@ import {
deleteProjectById,
getProjectTaskMasterById,
getProjectPathById,
clearProjectDirectoryCache,
searchConversations,
} from './projects.js';
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
@@ -68,149 +67,7 @@ import { getConnectableHost } from '../shared/networkHosts.js';
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
// 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') },
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
{ provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', '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
// Broadcast progress to all connected WebSocket clients
function broadcastProgress(progress) {
const message = JSON.stringify({
type: 'loading_progress',
...progress
});
connectedClients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
async function setupProjectsWatcher() {
const chokidar = (await import('chokidar')).default;
if (projectsWatcherDebounceTimer) {
clearTimeout(projectsWatcherDebounceTimer);
projectsWatcherDebounceTimer = null;
}
await Promise.all(
projectsWatchers.map(async (watcher) => {
try {
await watcher.close();
} catch (error) {
console.error('[WARN] Failed to close watcher:', error);
}
})
);
projectsWatchers = [];
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 getProjectsWithSessions(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 {
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
// Ensure provider folders exist before creating the watcher so watching stays active.
await fsPromises.mkdir(rootPath, { recursive: true });
// 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
}
});
// 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');
}
}
export const connectedClients = new Set();
const app = express();
const server = http.createServer(app);
@@ -219,6 +76,7 @@ const ptySessionsMap = new Map();
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
// Single WebSocket server that handles both paths
const wss = new WebSocketServer({
@@ -431,7 +289,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
app.get('/api/projects', authenticateToken, async (req, res) => {
try {
const projects = await getProjectsWithSessions(broadcastProgress);
const projects = await getProjectsWithSessions();
res.json(projects);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -2373,7 +2231,7 @@ async function startServer() {
console.log('');
// Start watching the projects folder for changes
await setupProjectsWatcher();
await initializeSessionsWatcher();
// await getProjectsWithSessions(); // TODO: REMOVE THIS
// Start server-side plugin processes for enabled plugins
@@ -2382,6 +2240,7 @@ async function startServer() {
});
});
await closeSessionsWatcher();
// Clean up plugin processes on shutdown
const shutdownPlugins = async () => {
await stopAllPlugins();