summaryrefslogtreecommitdiff
path: root/src/mcp/lattex.mjs
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-15 18:21:06 -0500
committerhaoyuren <13851610112@163.com>2026-03-15 18:21:06 -0500
commitc9d673d83037167553dcef3947065266743b2d5f (patch)
treeaa4d5c54da8db9a5d05052fa0b6771f0dbc7e6ee /src/mcp/lattex.mjs
parent90abc457f29f110dbf89f98efef5d9743efee963 (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/mcp/lattex.mjs')
-rw-r--r--src/mcp/lattex.mjs65
1 files changed, 59 insertions, 6 deletions
diff --git a/src/mcp/lattex.mjs b/src/mcp/lattex.mjs
index 356122e..861ff40 100644
--- a/src/mcp/lattex.mjs
+++ b/src/mcp/lattex.mjs
@@ -12,7 +12,7 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js'
-import { readFileSync, readdirSync, statSync } from 'fs'
+import { readFileSync, readdirSync, statSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import { join, relative } from 'path'
import https from 'https'
@@ -277,6 +277,11 @@ function buildOutputUrl(file, data) {
// ── Compile + fetch log helper ──────────────────────────────
async function compileAndFetchLog(projectId, cookie, csrf, pathDocMap, mainFile) {
+ // Flush in-memory OT changes to database so CLSI sees latest content
+ try {
+ await overleafRequest('POST', `/project/${projectId}/flush`, cookie, csrf)
+ } catch {}
+
const body = {
check: 'silent',
draft: false,
@@ -712,11 +717,59 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
case 'compile_latex': {
const mainFile = args?.main_file || null
+ const cwd = process.cwd()
+ const requestPath = join(cwd, '.lattex-compile-request')
+ const resultPath = join(cwd, '.lattex-compile-result')
+
+ // Clean up any stale result file
+ try { unlinkSync(resultPath) } catch {}
+
+ // Write compile request for the main process to pick up
+ const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6)
+ writeFileSync(requestPath, JSON.stringify({
+ requestId,
+ mainFile,
+ timestamp: Date.now()
+ }))
+
+ // Poll for result (main process compiles + downloads PDF + updates UI)
+ const timeout = 120000 // 2 minutes max
+ const pollInterval = 500
+ const start = Date.now()
+ let result = null
+
+ while (Date.now() - start < timeout) {
+ await new Promise(r => setTimeout(r, pollInterval))
+ try {
+ if (existsSync(resultPath)) {
+ result = JSON.parse(readFileSync(resultPath, 'utf-8'))
+ unlinkSync(resultPath)
+ break
+ }
+ } catch {}
+ }
- const { status } = await compileAndFetchLog(projectId, cookie, csrf, pathDocMap, mainFile)
+ if (!result) {
+ // Timeout — fall back to direct compile
+ try { unlinkSync(requestPath) } catch {}
+ const { status } = await compileAndFetchLog(projectId, cookie, csrf, pathDocMap, mainFile)
+ lastCompileStatus = status
+ if (status === 'success') {
+ return textResult('Compilation successful (direct, UI may not have updated).')
+ }
+ return textResult(`Compilation failed (status: ${status}). Use get_compile_log for details.`)
+ }
+
+ // Read compile log written by main process (avoids redundant compile API call)
+ lastCompileStatus = result.status || (result.success ? 'success' : 'failure')
+ const logPath = join(cwd, '.lattex-compile-log')
+ try {
+ lastCompileLog = readFileSync(logPath, 'utf-8')
+ } catch {
+ lastCompileLog = null
+ }
- if (status === 'success') {
- // Parse warnings for summary
+ if (result.success) {
if (lastCompileLog) {
const entries = parseCompileLog(lastCompileLog)
const warnings = entries.filter(e => e.level === 'warning')
@@ -735,7 +788,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const errors = entries.filter(e => e.level === 'error')
const warnings = entries.filter(e => e.level === 'warning')
- const summary = [`Compilation failed (status: ${status}).`]
+ const summary = [`Compilation failed (status: ${result.status || 'failure'}).`]
if (errors.length > 0) {
summary.push(`\n${errors.length} error(s):`)
for (const e of errors.slice(0, 10)) {
@@ -752,7 +805,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return textResult(summary.join('\n'))
}
- return textResult(`Compilation failed with status: ${status}. No log available.`)
+ return textResult(`Compilation failed (status: ${result.status || 'failure'}). No log available.`)
}
case 'get_compile_errors': {