mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-07 21:25:46 +00:00
refactor: use updated session watcher
In addition, for projects_updated websocket response, send the sessionId instead
This commit is contained in:
@@ -76,7 +76,7 @@ export const sessionsDb = {
|
||||
createdAt?: string,
|
||||
updatedAt?: string,
|
||||
jsonlPath?: string | null
|
||||
): void {
|
||||
): string {
|
||||
const db = getConnection();
|
||||
const createdAtValue = normalizeTimestamp(createdAt);
|
||||
const updatedAtValue = normalizeTimestamp(updatedAt);
|
||||
@@ -103,6 +103,8 @@ export const sessionsDb = {
|
||||
createdAtValue,
|
||||
updatedAtValue
|
||||
);
|
||||
|
||||
return sessionId;
|
||||
},
|
||||
|
||||
updateSessionCustomName(sessionId: string, customName: string): void {
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from 'node:path';
|
||||
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
||||
import { sessionSynchronizerService } from '@/modules/providers/index.js';
|
||||
import { findAppRoot, getModuleDir } from '@/utils/runtime-paths.js';
|
||||
import { connectedClients } from '@/index.js';
|
||||
|
||||
type SessionSummary = {
|
||||
id: string;
|
||||
@@ -35,9 +36,12 @@ export type ProjectsSnapshot = {
|
||||
projects: ProjectListItem[];
|
||||
};
|
||||
|
||||
type ProgressCallback =
|
||||
| ((progress: { phase: 'loading' | 'complete'; current: number; total: number; currentProject?: string }) => void)
|
||||
| null;
|
||||
type ProgressUpdate = {
|
||||
phase: 'loading' | 'complete';
|
||||
current: number;
|
||||
total: number;
|
||||
currentProject?: string;
|
||||
};
|
||||
|
||||
const __dirname = getModuleDir(import.meta.url);
|
||||
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.
|
||||
*/
|
||||
export async function getProjectsWithSessions(progressCallback: ProgressCallback = null): Promise<ProjectListItem[]> {
|
||||
export async function getProjectsWithSessions(): Promise<ProjectListItem[]> {
|
||||
await sessionSynchronizerService.synchronizeSessions();
|
||||
|
||||
const projectRows = projectsDb.getProjectPaths() as Array<{
|
||||
@@ -193,14 +211,12 @@ export async function getProjectsWithSessions(progressCallback: ProgressCallback
|
||||
const projectId = row.project_id;
|
||||
const projectPath = row.project_path;
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
phase: 'loading',
|
||||
current: processedProjects,
|
||||
total: totalProjects,
|
||||
currentProject: projectPath,
|
||||
});
|
||||
}
|
||||
broadcastProgress({
|
||||
phase: 'loading',
|
||||
current: processedProjects,
|
||||
total: totalProjects,
|
||||
currentProject: projectPath,
|
||||
});
|
||||
|
||||
const displayName =
|
||||
row.custom_project_name && row.custom_project_name.trim().length > 0
|
||||
@@ -227,13 +243,11 @@ export async function getProjectsWithSessions(progressCallback: ProgressCallback
|
||||
});
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback({
|
||||
phase: 'complete',
|
||||
current: totalProjects,
|
||||
total: totalProjects,
|
||||
});
|
||||
}
|
||||
broadcastProgress({
|
||||
phase: 'complete',
|
||||
current: totalProjects,
|
||||
total: totalProjects,
|
||||
});
|
||||
|
||||
await writeSnapshot(projects);
|
||||
return projects;
|
||||
|
||||
@@ -61,19 +61,19 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
/**
|
||||
* 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')) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
|
||||
const parsed = await this.processSessionFile(filePath, nameMap);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamps = await readFileTimestamps(filePath);
|
||||
sessionsDb.createSession(
|
||||
return sessionsDb.createSession(
|
||||
parsed.sessionId,
|
||||
this.provider,
|
||||
parsed.projectPath,
|
||||
@@ -82,8 +82,6 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
timestamps.updatedAt,
|
||||
filePath
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,19 +61,19 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
/**
|
||||
* 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')) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
|
||||
const parsed = await this.processSessionFile(filePath, nameMap);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamps = await readFileTimestamps(filePath);
|
||||
sessionsDb.createSession(
|
||||
return sessionsDb.createSession(
|
||||
parsed.sessionId,
|
||||
this.provider,
|
||||
parsed.projectPath,
|
||||
@@ -82,8 +82,6 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
timestamps.updatedAt,
|
||||
filePath
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,18 +91,18 @@ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
/**
|
||||
* 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')) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = await this.processSessionFile(filePath);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamps = await readFileTimestamps(filePath);
|
||||
sessionsDb.createSession(
|
||||
return sessionsDb.createSession(
|
||||
parsed.sessionId,
|
||||
this.provider,
|
||||
parsed.projectPath,
|
||||
@@ -111,8 +111,6 @@ export class CursorSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
timestamps.updatedAt,
|
||||
filePath
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,25 +72,25 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
/**
|
||||
* 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')) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
filePath.startsWith(path.join(this.geminiHome, 'tmp'))
|
||||
&& !filePath.includes(`${path.sep}chats${path.sep}`)
|
||||
) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = await this.processSessionFile(filePath);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const timestamps = await readFileTimestamps(filePath);
|
||||
sessionsDb.createSession(
|
||||
return sessionsDb.createSession(
|
||||
parsed.sessionId,
|
||||
this.provider,
|
||||
parsed.projectPath,
|
||||
@@ -99,8 +99,6 @@ export class GeminiSessionSynchronizer implements IProviderSessionSynchronizer {
|
||||
timestamps.updatedAt,
|
||||
filePath
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,10 +168,14 @@ export const sessionSynchronizerService = {
|
||||
async synchronizeProviderFile(
|
||||
provider: LLMProvider,
|
||||
filePath: string
|
||||
): Promise<{ provider: LLMProvider; indexed: boolean }> {
|
||||
): Promise<{ provider: LLMProvider; indexed: boolean; sessionId: string | null }> {
|
||||
const resolvedProvider = providerRegistry.resolveProvider(provider);
|
||||
const indexed = await resolvedProvider.sessionSynchronizer.synchronizeFile(filePath);
|
||||
return { provider, indexed };
|
||||
const sessionId = await resolvedProvider.sessionSynchronizer.synchronizeFile(filePath);
|
||||
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 type { LLMProvider } from '@/shared/types.js';
|
||||
import { getProjectsWithSessions } from '@/modules/projects/index.js';
|
||||
import { connectedClients } from '@/index.js';
|
||||
|
||||
type WatcherEventType = 'add' | 'change';
|
||||
|
||||
@@ -43,6 +45,7 @@ const WATCHER_IGNORED_PATTERNS = [
|
||||
];
|
||||
|
||||
const watchers: FSWatcher[] = [];
|
||||
const WS_OPEN_STATE = 1;
|
||||
|
||||
/**
|
||||
* Filters watcher events to provider-specific session artifact file types.
|
||||
@@ -69,9 +72,31 @@ async function onUpdate(
|
||||
|
||||
try {
|
||||
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}`, {
|
||||
filePath,
|
||||
indexed: result.indexed,
|
||||
sessionId: result.sessionId,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
Reference in New Issue
Block a user