diff options
| author | haoyuren <13851610112@163.com> | 2026-03-15 18:21:06 -0500 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-15 18:21:06 -0500 |
| commit | c9d673d83037167553dcef3947065266743b2d5f (patch) | |
| tree | aa4d5c54da8db9a5d05052fa0b6771f0dbc7e6ee /src/main/index.ts | |
| parent | 90abc457f29f110dbf89f98efef5d9743efee963 (diff) | |
Fix file sync for non-active tabs, MCP compile integration, OT resilience
- Fix .bib (and other non-active tab) edits disappearing: call otLeaveDoc
on tab switch so bridge takes back OT ownership; release .bib pre-loads
immediately after reading content for citation autocomplete
- Always update lastKnownContent in processDocChange for editor docs to
prevent stale state accumulation
- Flush pending OT ops in OverleafDocSync.destroy() before tab switch
- Add three-way merge in replaceContent to preserve concurrent remote edits
- Wire MCP compile to UI: file-based signal between MCP server and Electron
main process, with compile animation and PDF refresh in renderer
- Add CLSI flush before compile to prevent stale cached results
- Add OT error recovery: re-join doc and re-apply disk changes on otUpdateError
- Add bridge reconnect handling: reset OtClient on docRejoined for non-editor docs
- Add compile concurrency lock to prevent duplicate compiles
- removeEditorDoc compares disk vs server content to catch in-flight ops
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main/index.ts')
| -rw-r--r-- | src/main/index.ts | 116 |
1 files changed, 112 insertions, 4 deletions
diff --git a/src/main/index.ts b/src/main/index.ts index 78a7604..5d29897 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -745,6 +745,9 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { fileSyncBridge = new FileSyncBridge(overleafSock, tmpDir, docPathMap, pathDocMap, fileRefs, mainWindow!, projectId, overleafSessionCookie, overleafCsrfToken) await fileSyncBridge.start() + // Start MCP compile watcher (detects compile requests from Claude Code) + startMcpCompileWatcher(tmpDir) + // Write MCP state + config for Claude Code integration mcpStateDir = tmpDir mcpProjectId = projectId @@ -926,7 +929,8 @@ You have MCP tools to interact with Overleaf. Use them proactively. }) ipcMain.handle('ot:disconnect', async () => { - // Clean up MCP state file + // Clean up MCP state file + compile watcher + stopMcpCompileWatcher() if (mcpStateDir) { unlink(join(mcpStateDir, '.lattex-mcp.json')).catch(() => {}) } @@ -1347,8 +1351,26 @@ ipcMain.handle('overleaf:socketCompile', async (_e, mainTexRelPath: string) => { }) }) -// Server-side compile via Overleaf's CLSI -ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { +// Server-side compile via Overleaf's CLSI (shared by IPC handler + MCP compile watcher) +let compileInProgress: Promise<{ success: boolean; log: string; pdfPath: string }> | null = null + +async function doServerCompile(rootDocId?: string): Promise<{ success: boolean; log: string; pdfPath: string }> { + // Prevent concurrent compiles — wait for existing one if already in progress + if (compileInProgress) { + console.log('[compile] compile already in progress, waiting...') + return compileInProgress + } + + const promise = doServerCompileImpl(rootDocId) + compileInProgress = promise + try { + return await promise + } finally { + compileInProgress = null + } +} + +async function doServerCompileImpl(rootDocId?: string): Promise<{ success: boolean; log: string; pdfPath: string }> { if (!overleafSessionCookie || !overleafSock?.projectData) { return { success: false, log: 'Not connected', pdfPath: '' } } @@ -1366,6 +1388,13 @@ ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { try { sendToRenderer('latex:log', 'Compiling on Overleaf server...\n') + // Flush in-memory OT changes to database so CLSI sees latest content + try { + await overleafFetch(`/project/${projectId}/flush`, { method: 'POST' }) + } catch (e) { + console.log('[compile] flush failed (non-fatal):', e) + } + const compileBody = JSON.stringify({ rootDoc_id: effectiveRootDocId, ...(rootResourcePath && { rootResourcePath }), @@ -1415,7 +1444,10 @@ ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { if (logFile) { try { const logContent = await fetchBinary(buildOutputUrl(logFile), overleafSessionCookie) - sendToRenderer('latex:log', Buffer.from(logContent).toString('utf-8')) + const logText = Buffer.from(logContent).toString('utf-8') + sendToRenderer('latex:log', logText) + // Write log for MCP server to read (avoids redundant compile API call) + writeFile(join(syncDir, '.lattex-compile-log'), logText).catch(() => {}) } catch (e) { sendToRenderer('latex:log', `[log fetch failed: ${e}]\n`) } @@ -1485,8 +1517,83 @@ ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { sendToRenderer('latex:log', msg + '\n') return { success: false, log: msg, pdfPath: '' } } +} + +ipcMain.handle('overleaf:serverCompile', async (_e, rootDocId?: string) => { + return doServerCompile(rootDocId) }) +// Watch for MCP compile requests (file-based signal from MCP server process) +let mcpCompileWatcher: ReturnType<typeof import('fs').watchFile> | null = null +let mcpCompileActive = false + +function startMcpCompileWatcher(syncDir: string) { + const requestPath = join(syncDir, '.lattex-compile-request') + const resultPath = join(syncDir, '.lattex-compile-result') + + // Poll for the request file every 300ms + const { watchFile, unwatchFile } = require('fs') + watchFile(requestPath, { interval: 300 }, async (curr: { size: number }) => { + if (curr.size === 0 || mcpCompileActive) return + mcpCompileActive = true + + try { + const reqData = JSON.parse(await readFile(requestPath, 'utf-8')) + await unlink(requestPath).catch(() => {}) + + console.log('[mcp-compile] compile request received:', reqData.requestId) + + // Notify renderer: compile started + sendToRenderer('compile:mcpStarted', null) + + // Resolve main_file to rootDocId if provided + let rootDocId: string | undefined + if (reqData.mainFile && mcpPathDocMap[reqData.mainFile]) { + rootDocId = mcpPathDocMap[reqData.mainFile] + } + + const result = await doServerCompile(rootDocId) + + // Notify renderer: compile finished (renderer will update PDF + compiling state) + sendToRenderer('compile:mcpFinished', { + success: result.success, + pdfPath: result.pdfPath + }) + + // Write result for MCP server to read + await writeFile(resultPath, JSON.stringify({ + requestId: reqData.requestId, + success: result.success, + pdfPath: result.pdfPath, + status: result.success ? 'success' : 'failure' + })) + console.log('[mcp-compile] compile result written:', result.success) + } catch (e) { + console.log('[mcp-compile] error handling compile request:', e) + // Write error result so MCP doesn't hang + await writeFile(resultPath, JSON.stringify({ + success: false, + status: 'error', + error: String(e) + })).catch(() => {}) + sendToRenderer('compile:mcpFinished', { success: false, pdfPath: '' }) + } finally { + mcpCompileActive = false + } + }) + + mcpCompileWatcher = { requestPath } as any + console.log('[mcp-compile] watcher started for', requestPath) +} + +function stopMcpCompileWatcher() { + if (mcpCompileWatcher) { + const { unwatchFile } = require('fs') + unwatchFile((mcpCompileWatcher as any).requestPath) + mcpCompileWatcher = null + } +} + /** Fetch a binary resource. Cookie is optional — CDN URLs use build ID for auth. */ function fetchBinary(url: string, cookie?: string): Promise<ArrayBuffer> { return new Promise((resolve, reject) => { @@ -1539,6 +1646,7 @@ app.whenReady().then(async () => { app.on('window-all-closed', () => { mainWindow = null + stopMcpCompileWatcher() for (const inst of ptyInstances.values()) inst.kill() ptyInstances.clear() fileSyncBridge?.stop() |
