mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 17:16:19 +00:00
163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
import chokidar from "chokidar";
|
|
import path from "path";
|
|
import os from "os";
|
|
import { promises as fsPromises } from "fs";
|
|
import { logger } from "@/shared/utils/logger.js";
|
|
import { getSessions } from "@/modules/sessions/sessions.service.js";
|
|
import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-parser.js";
|
|
import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-parser.js";
|
|
import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-parser.js";
|
|
import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-parser.js";
|
|
import { sessionsDb } from "@/shared/database/repositories/sessions.db.js";
|
|
import { LLMProvider } from "@/shared/types/app.js";
|
|
|
|
let projectsWatchers: any[] = [];
|
|
|
|
// File system watchers for provider project/session folders
|
|
const PROVIDER_WATCH_PATHS: { provider: LLMProvider; rootPath: string }[] = [
|
|
{
|
|
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", "sessions"),
|
|
},
|
|
];
|
|
|
|
const WATCHER_IGNORED_PATTERNS = [
|
|
"**/node_modules/**",
|
|
"**/.git/**",
|
|
"**/dist/**",
|
|
"**/build/**",
|
|
"**/*.tmp",
|
|
"**/*.swp",
|
|
"**/.DS_Store",
|
|
];
|
|
|
|
type EventType = "add" | "change";
|
|
|
|
const onUpdate = async (
|
|
eventType: EventType,
|
|
filePath: string,
|
|
provider: LLMProvider,
|
|
) => {
|
|
try {
|
|
console.log("[eventType] detected: ", eventType, " filePath: ", filePath, " provider: ", provider);
|
|
|
|
switch (eventType) {
|
|
case "add":
|
|
case "change": {
|
|
let sessionId: string | null = null;
|
|
let workspacePath: string | null = null;
|
|
let sessionName = `Untitled ${provider} Session`;
|
|
|
|
switch (provider) {
|
|
case "claude": {
|
|
const result = await processClaudeSessionFile(filePath);
|
|
if (result) {
|
|
sessionId = result.sessionId;
|
|
workspacePath = result.workspacePath;
|
|
sessionName = result.sessionName || sessionName;
|
|
}
|
|
break;
|
|
}
|
|
case "codex": {
|
|
const result = await processCodexSessionFile(filePath);
|
|
if (result) {
|
|
sessionId = result.sessionId;
|
|
workspacePath = result.workspacePath;
|
|
sessionName = result.sessionName || sessionName;
|
|
}
|
|
break;
|
|
}
|
|
case "gemini": {
|
|
const result = await processGeminiSessionFile(filePath);
|
|
if (result) {
|
|
sessionId = result.sessionId;
|
|
workspacePath = result.workspacePath;
|
|
sessionName = result.sessionName || sessionName;
|
|
}
|
|
break;
|
|
}
|
|
case "cursor": {
|
|
const result = await processCursorSessionFile(filePath);
|
|
if (result) {
|
|
sessionId = result.sessionId;
|
|
workspacePath = result.workspacePath;
|
|
sessionName = result.sessionName || sessionName;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sessionId && workspacePath) {
|
|
sessionsDb.createSession(sessionId, provider, workspacePath, sessionName);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
logger.error(
|
|
`[ERROR] Failed to handle ${provider} file change for ${filePath}:`,
|
|
error,
|
|
);
|
|
}
|
|
};
|
|
|
|
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
export async function initializeWatcher() {
|
|
logger.info("Setting up project watchers for providers...");
|
|
|
|
await getSessions();
|
|
|
|
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 });
|
|
|
|
logger.info(`Setting up watcher for ${provider} at: ${rootPath}`);
|
|
|
|
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: 6, // Reasonable depth limit
|
|
usePolling: true, // Use polling to fix Windows fs.watch buffering/batching issues. It now stops relying on the OS's native file-system events and instead manually checks the files for changes at a set interval.
|
|
interval: 2000, // Poll every 2000ms
|
|
binaryInterval: 6000, // We set a high amount because checking large binary files for changes using polling is much more CPU-intensive than checking small text files.
|
|
// Removed awaitWriteFinish to prevent delays when LLM streams to the file
|
|
|
|
});
|
|
|
|
// Set up event listeners
|
|
watcher
|
|
.on("add", (filePath) => onUpdate("add", filePath, provider))
|
|
.on("change", (filePath) =>
|
|
onUpdate("change", filePath, provider),
|
|
)
|
|
.on("error", (error: any) => {
|
|
logger.error(`[ERROR] ${provider} watcher error: ${error.message}`);
|
|
})
|
|
.on("ready", () => { });
|
|
|
|
projectsWatchers.push(watcher);
|
|
} catch (error: any) {
|
|
logger.error(
|
|
`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`,
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|