mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-06 13:15:38 +08:00
refactor: use updated session watcher
In addition, for projects_updated websocket response, send the sessionId instead
This commit is contained in:
151
server/index.js
151
server/index.js
@@ -36,7 +36,6 @@ import {
|
|||||||
deleteProjectById,
|
deleteProjectById,
|
||||||
getProjectTaskMasterById,
|
getProjectTaskMasterById,
|
||||||
getProjectPathById,
|
getProjectPathById,
|
||||||
clearProjectDirectoryCache,
|
|
||||||
searchConversations,
|
searchConversations,
|
||||||
} from './projects.js';
|
} from './projects.js';
|
||||||
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.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'];
|
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
||||||
|
|
||||||
// File system watchers for provider project/session folders
|
export const connectedClients = new Set();
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
@@ -219,6 +76,7 @@ const ptySessionsMap = new Map();
|
|||||||
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
||||||
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
||||||
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
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
|
// Single WebSocket server that handles both paths
|
||||||
const wss = new WebSocketServer({
|
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) => {
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const projects = await getProjectsWithSessions(broadcastProgress);
|
const projects = await getProjectsWithSessions();
|
||||||
res.json(projects);
|
res.json(projects);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -2373,7 +2231,7 @@ async function startServer() {
|
|||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Start watching the projects folder for changes
|
// Start watching the projects folder for changes
|
||||||
await setupProjectsWatcher();
|
await initializeSessionsWatcher();
|
||||||
|
|
||||||
// await getProjectsWithSessions(); // TODO: REMOVE THIS
|
// await getProjectsWithSessions(); // TODO: REMOVE THIS
|
||||||
// Start server-side plugin processes for enabled plugins
|
// Start server-side plugin processes for enabled plugins
|
||||||
@@ -2382,6 +2240,7 @@ async function startServer() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await closeSessionsWatcher();
|
||||||
// Clean up plugin processes on shutdown
|
// Clean up plugin processes on shutdown
|
||||||
const shutdownPlugins = async () => {
|
const shutdownPlugins = async () => {
|
||||||
await stopAllPlugins();
|
await stopAllPlugins();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const sessionsDb = {
|
|||||||
createdAt?: string,
|
createdAt?: string,
|
||||||
updatedAt?: string,
|
updatedAt?: string,
|
||||||
jsonlPath?: string | null
|
jsonlPath?: string | null
|
||||||
): void {
|
): string {
|
||||||
const db = getConnection();
|
const db = getConnection();
|
||||||
const createdAtValue = normalizeTimestamp(createdAt);
|
const createdAtValue = normalizeTimestamp(createdAt);
|
||||||
const updatedAtValue = normalizeTimestamp(updatedAt);
|
const updatedAtValue = normalizeTimestamp(updatedAt);
|
||||||
@@ -103,6 +103,8 @@ export const sessionsDb = {
|
|||||||
createdAtValue,
|
createdAtValue,
|
||||||
updatedAtValue
|
updatedAtValue
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return sessionId;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSessionCustomName(sessionId: string, customName: string): void {
|
updateSessionCustomName(sessionId: string, customName: string): void {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|||||||
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
||||||
import { sessionSynchronizerService } from '@/modules/providers/index.js';
|
import { sessionSynchronizerService } from '@/modules/providers/index.js';
|
||||||
import { findAppRoot, getModuleDir } from '@/utils/runtime-paths.js';
|
import { findAppRoot, getModuleDir } from '@/utils/runtime-paths.js';
|
||||||
|
import { connectedClients } from '@/index.js';
|
||||||
|
|
||||||
type SessionSummary = {
|
type SessionSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -35,9 +36,12 @@ export type ProjectsSnapshot = {
|
|||||||
projects: ProjectListItem[];
|
projects: ProjectListItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProgressCallback =
|
type ProgressUpdate = {
|
||||||
| ((progress: { phase: 'loading' | 'complete'; current: number; total: number; currentProject?: string }) => void)
|
phase: 'loading' | 'complete';
|
||||||
| null;
|
current: number;
|
||||||
|
total: number;
|
||||||
|
currentProject?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const __dirname = getModuleDir(import.meta.url);
|
const __dirname = getModuleDir(import.meta.url);
|
||||||
const APP_ROOT = findAppRoot(__dirname);
|
const APP_ROOT = findAppRoot(__dirname);
|
||||||
@@ -172,10 +176,24 @@ export async function writeSnapshot(projects: ProjectListItem[]): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Broadcast progress to all connected WebSocket clients
|
||||||
|
function broadcastProgress(progress: ProgressUpdate) {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'loading_progress',
|
||||||
|
...progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
connectedClients.forEach((client: any) => {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads all projects from DB and returns provider-bucketed session summaries.
|
* Reads all projects from DB and returns provider-bucketed session summaries.
|
||||||
*/
|
*/
|
||||||
export async function getProjectsWithSessions(progressCallback: ProgressCallback = null): Promise<ProjectListItem[]> {
|
export async function getProjectsWithSessions(): Promise<ProjectListItem[]> {
|
||||||
await sessionSynchronizerService.synchronizeSessions();
|
await sessionSynchronizerService.synchronizeSessions();
|
||||||
|
|
||||||
const projectRows = projectsDb.getProjectPaths() as Array<{
|
const projectRows = projectsDb.getProjectPaths() as Array<{
|
||||||
@@ -193,14 +211,12 @@ export async function getProjectsWithSessions(progressCallback: ProgressCallback
|
|||||||
const projectId = row.project_id;
|
const projectId = row.project_id;
|
||||||
const projectPath = row.project_path;
|
const projectPath = row.project_path;
|
||||||
|
|
||||||
if (progressCallback) {
|
broadcastProgress({
|
||||||
progressCallback({
|
phase: 'loading',
|
||||||
phase: 'loading',
|
current: processedProjects,
|
||||||
current: processedProjects,
|
total: totalProjects,
|
||||||
total: totalProjects,
|
currentProject: projectPath,
|
||||||
currentProject: projectPath,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName =
|
const displayName =
|
||||||
row.custom_project_name && row.custom_project_name.trim().length > 0
|
row.custom_project_name && row.custom_project_name.trim().length > 0
|
||||||
@@ -227,13 +243,11 @@ export async function getProjectsWithSessions(progressCallback: ProgressCallback
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
broadcastProgress({
|
||||||
progressCallback({
|
phase: 'complete',
|
||||||
phase: 'complete',
|
current: totalProjects,
|
||||||
current: totalProjects,
|
total: totalProjects,
|
||||||
total: totalProjects,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeSnapshot(projects);
|
await writeSnapshot(projects);
|
||||||
return projects;
|
return projects;
|
||||||
|
|||||||
@@ -61,19 +61,19 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
/**
|
/**
|
||||||
* Parses and upserts one Claude session JSONL file.
|
* Parses and upserts one Claude session JSONL file.
|
||||||
*/
|
*/
|
||||||
async synchronizeFile(filePath: string): Promise<boolean> {
|
async synchronizeFile(filePath: string): Promise<string | null> {
|
||||||
if (!filePath.endsWith('.jsonl')) {
|
if (!filePath.endsWith('.jsonl')) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
|
const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
|
||||||
const parsed = await this.processSessionFile(filePath, nameMap);
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = await readFileTimestamps(filePath);
|
const timestamps = await readFileTimestamps(filePath);
|
||||||
sessionsDb.createSession(
|
return sessionsDb.createSession(
|
||||||
parsed.sessionId,
|
parsed.sessionId,
|
||||||
this.provider,
|
this.provider,
|
||||||
parsed.projectPath,
|
parsed.projectPath,
|
||||||
@@ -82,8 +82,6 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
timestamps.updatedAt,
|
timestamps.updatedAt,
|
||||||
filePath
|
filePath
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -61,19 +61,19 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
/**
|
/**
|
||||||
* Parses and upserts one Codex session JSONL file.
|
* Parses and upserts one Codex session JSONL file.
|
||||||
*/
|
*/
|
||||||
async synchronizeFile(filePath: string): Promise<boolean> {
|
async synchronizeFile(filePath: string): Promise<string | null> {
|
||||||
if (!filePath.endsWith('.jsonl')) {
|
if (!filePath.endsWith('.jsonl')) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
|
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
|
||||||
const parsed = await this.processSessionFile(filePath, nameMap);
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = await readFileTimestamps(filePath);
|
const timestamps = await readFileTimestamps(filePath);
|
||||||
sessionsDb.createSession(
|
return sessionsDb.createSession(
|
||||||
parsed.sessionId,
|
parsed.sessionId,
|
||||||
this.provider,
|
this.provider,
|
||||||
parsed.projectPath,
|
parsed.projectPath,
|
||||||
@@ -82,8 +82,6 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
timestamps.updatedAt,
|
timestamps.updatedAt,
|
||||||
filePath
|
filePath
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -91,18 +91,18 @@ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
/**
|
/**
|
||||||
* Parses and upserts one Cursor session JSONL file.
|
* Parses and upserts one Cursor session JSONL file.
|
||||||
*/
|
*/
|
||||||
async synchronizeFile(filePath: string): Promise<boolean> {
|
async synchronizeFile(filePath: string): Promise<string | null> {
|
||||||
if (!filePath.endsWith('.jsonl')) {
|
if (!filePath.endsWith('.jsonl')) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = await this.processSessionFile(filePath);
|
const parsed = await this.processSessionFile(filePath);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = await readFileTimestamps(filePath);
|
const timestamps = await readFileTimestamps(filePath);
|
||||||
sessionsDb.createSession(
|
return sessionsDb.createSession(
|
||||||
parsed.sessionId,
|
parsed.sessionId,
|
||||||
this.provider,
|
this.provider,
|
||||||
parsed.projectPath,
|
parsed.projectPath,
|
||||||
@@ -111,8 +111,6 @@ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
timestamps.updatedAt,
|
timestamps.updatedAt,
|
||||||
filePath
|
filePath
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -72,25 +72,25 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
/**
|
/**
|
||||||
* Parses and upserts one Gemini session JSON artifact.
|
* Parses and upserts one Gemini session JSON artifact.
|
||||||
*/
|
*/
|
||||||
async synchronizeFile(filePath: string): Promise<boolean> {
|
async synchronizeFile(filePath: string): Promise<string | null> {
|
||||||
if (!filePath.endsWith('.json')) {
|
if (!filePath.endsWith('.json')) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
filePath.startsWith(path.join(this.geminiHome, 'tmp'))
|
filePath.startsWith(path.join(this.geminiHome, 'tmp'))
|
||||||
&& !filePath.includes(`${path.sep}chats${path.sep}`)
|
&& !filePath.includes(`${path.sep}chats${path.sep}`)
|
||||||
) {
|
) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = await this.processSessionFile(filePath);
|
const parsed = await this.processSessionFile(filePath);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timestamps = await readFileTimestamps(filePath);
|
const timestamps = await readFileTimestamps(filePath);
|
||||||
sessionsDb.createSession(
|
return sessionsDb.createSession(
|
||||||
parsed.sessionId,
|
parsed.sessionId,
|
||||||
this.provider,
|
this.provider,
|
||||||
parsed.projectPath,
|
parsed.projectPath,
|
||||||
@@ -99,8 +99,6 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
|
|||||||
timestamps.updatedAt,
|
timestamps.updatedAt,
|
||||||
filePath
|
filePath
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -168,10 +168,14 @@ export const sessionSynchronizerService = {
|
|||||||
async synchronizeProviderFile(
|
async synchronizeProviderFile(
|
||||||
provider: LLMProvider,
|
provider: LLMProvider,
|
||||||
filePath: string
|
filePath: string
|
||||||
): Promise<{ provider: LLMProvider; indexed: boolean }> {
|
): Promise<{ provider: LLMProvider; indexed: boolean; sessionId: string | null }> {
|
||||||
const resolvedProvider = providerRegistry.resolveProvider(provider);
|
const resolvedProvider = providerRegistry.resolveProvider(provider);
|
||||||
const indexed = await resolvedProvider.sessionSynchronizer.synchronizeFile(filePath);
|
const sessionId = await resolvedProvider.sessionSynchronizer.synchronizeFile(filePath);
|
||||||
return { provider, indexed };
|
return {
|
||||||
|
provider,
|
||||||
|
indexed: Boolean(sessionId),
|
||||||
|
sessionId,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import chokidar, { type FSWatcher } from 'chokidar';
|
|||||||
|
|
||||||
import { sessionSynchronizerService } from '@/modules/providers/services/session-synchronizer.service.js';
|
import { sessionSynchronizerService } from '@/modules/providers/services/session-synchronizer.service.js';
|
||||||
import type { LLMProvider } from '@/shared/types.js';
|
import type { LLMProvider } from '@/shared/types.js';
|
||||||
|
import { getProjectsWithSessions } from '@/modules/projects/index.js';
|
||||||
|
import { connectedClients } from '@/index.js';
|
||||||
|
|
||||||
type WatcherEventType = 'add' | 'change';
|
type WatcherEventType = 'add' | 'change';
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ const WATCHER_IGNORED_PATTERNS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const watchers: FSWatcher[] = [];
|
const watchers: FSWatcher[] = [];
|
||||||
|
const WS_OPEN_STATE = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters watcher events to provider-specific session artifact file types.
|
* Filters watcher events to provider-specific session artifact file types.
|
||||||
@@ -69,9 +72,31 @@ async function onUpdate(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await sessionSynchronizerService.synchronizeProviderFile(provider, filePath);
|
const result = await sessionSynchronizerService.synchronizeProviderFile(provider, filePath);
|
||||||
|
|
||||||
|
// Get updated projects list
|
||||||
|
const updatedProjects = await getProjectsWithSessions();
|
||||||
|
|
||||||
|
// Notify all connected clients about the project changes
|
||||||
|
const updateMessage = JSON.stringify({
|
||||||
|
type: 'projects_updated',
|
||||||
|
projects: updatedProjects,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
changeType: eventType,
|
||||||
|
updatedSessionId: result.sessionId ?? undefined,
|
||||||
|
watchProvider: provider
|
||||||
|
});
|
||||||
|
|
||||||
|
connectedClients.forEach(client => {
|
||||||
|
if (client.readyState === WS_OPEN_STATE) {
|
||||||
|
client.send(updateMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
console.log(`Session watcher sync complete for provider "${provider}" after ${eventType}`, {
|
console.log(`Session watcher sync complete for provider "${provider}" after ${eventType}`, {
|
||||||
filePath,
|
filePath,
|
||||||
indexed: result.indexed,
|
indexed: result.indexed,
|
||||||
|
sessionId: result.sessionId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
|||||||
@@ -88,5 +88,5 @@ export interface IProviderSessionSynchronizer {
|
|||||||
/**
|
/**
|
||||||
* Parses and upserts one provider artifact file without running a full scan.
|
* Parses and upserts one provider artifact file without running a full scan.
|
||||||
*/
|
*/
|
||||||
synchronizeFile(filePath: string): Promise<boolean>;
|
synchronizeFile(filePath: string): Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,20 +311,12 @@ export function useProjectsState({
|
|||||||
|
|
||||||
const projectsMessage = latestMessage as ProjectsUpdatedMessage;
|
const projectsMessage = latestMessage as ProjectsUpdatedMessage;
|
||||||
|
|
||||||
if (projectsMessage.changedFile && selectedSession && selectedProject) {
|
if (projectsMessage.updatedSessionId && selectedSession && selectedProject) {
|
||||||
const normalized = projectsMessage.changedFile.replace(/\\/g, '/');
|
if (projectsMessage.updatedSessionId === selectedSession.id) {
|
||||||
const changedFileParts = normalized.split('/');
|
const isSessionActive = activeSessions.has(selectedSession.id);
|
||||||
|
|
||||||
if (changedFileParts.length >= 2) {
|
if (!isSessionActive) {
|
||||||
const filename = changedFileParts[changedFileParts.length - 1];
|
setExternalMessageUpdate((prev) => prev + 1);
|
||||||
const changedSessionId = filename.replace('.jsonl', '');
|
|
||||||
|
|
||||||
if (changedSessionId === selectedSession.id) {
|
|
||||||
const isSessionActive = activeSessions.has(selectedSession.id);
|
|
||||||
|
|
||||||
if (!isSessionActive) {
|
|
||||||
setExternalMessageUpdate((prev) => prev + 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export interface LoadingProgress {
|
|||||||
export interface ProjectsUpdatedMessage {
|
export interface ProjectsUpdatedMessage {
|
||||||
type: 'projects_updated';
|
type: 'projects_updated';
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
changedFile?: string;
|
updatedSessionId?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user