summaryrefslogtreecommitdiff
path: root/src/main/index.ts
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-18 08:06:32 +0000
committerhaoyuren <13851610112@163.com>2026-03-18 08:06:32 +0000
commit9b5256718c2117511f0253a656bb8cff7410b92a (patch)
tree8ba0fd257f771538874f37b87dcaeb5471185ca5 /src/main/index.ts
parent69a09baf71798966724d942b93303211516e34c7 (diff)
Fix OT sync corruption: match Overleaf ShareJS ack/echo handling
The server broadcasts otUpdateApplied (with ops) to ALL clients including the sender. Our bridge was treating its own echoed ops as remote ops and re-applying them, causing text duplication (e.g. "simulatorimulator"). Rewrite OT handling to match Overleaf's ShareJS _onMessage pattern: - ACK = no ops OR meta.source matches our publicId (own echo) - REMOTE = ops from a different source - ACK path calls onAck() without re-applying ops - OtClient silently drops duplicate acks in synchronized state - OtClient drops stale remote ops (version < current) - Remove pendingEchos counter in favor of meta.source detection Also: refresh MCP comment contexts on new-comment/delete-thread events, add Overleaf reference repo to .gitignore. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/main/index.ts')
-rw-r--r--src/main/index.ts32
1 files changed, 30 insertions, 2 deletions
diff --git a/src/main/index.ts b/src/main/index.ts
index 5efdae5..d2c3d64 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -47,6 +47,31 @@ async function writeMcpState(): Promise<void> {
} catch { /* ignore */ }
}
+let commentContextRefreshTimer: ReturnType<typeof setTimeout> | null = null
+function scheduleCommentContextRefresh(): void {
+ if (commentContextRefreshTimer) clearTimeout(commentContextRefreshTimer)
+ commentContextRefreshTimer = setTimeout(async () => {
+ commentContextRefreshTimer = null
+ if (!overleafSock?.projectData) return
+ const { docPathMap: dp } = walkRootFolder(overleafSock.projectData.project.rootFolder)
+ const contexts: Record<string, { file: string; text: string; pos: number }> = {}
+ for (const [did, rp] of Object.entries(dp)) {
+ try {
+ const result = await overleafSock.joinDoc(did)
+ if (result.ranges?.comments) {
+ for (const c of result.ranges.comments) {
+ if (c.op?.t) contexts[c.op.t] = { file: rp, text: c.op.c || '', pos: c.op.p || 0 }
+ }
+ }
+ // Don't leaveDoc — bridge keeps all docs joined
+ } catch { /* ignore */ }
+ }
+ mcpCommentContexts = contexts
+ writeMcpState()
+ sendToRenderer('comments:initContexts', { contexts })
+ }, 2000) // 2s debounce
+}
+
function writeMcpOnlineUsers(): void {
if (!mcpStateDir) return
if (mcpOnlineUsersWriteTimer) clearTimeout(mcpOnlineUsersWriteTimer)
@@ -776,6 +801,10 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => {
name === 'delete-message'
) {
sendToRenderer('comments:event', { type: name, args })
+ // Re-fetch comment contexts for MCP when comments change
+ if (name === 'new-comment' || name === 'delete-thread') {
+ scheduleCommentContextRefresh()
+ }
}
})
@@ -968,14 +997,13 @@ The \`claude-workspace/\` directory is your private scratch space. It is **not s
const contexts: Record<string, { file: string; text: string; pos: number }> = {}
for (const [did, rp] of Object.entries(dp)) {
try {
- const alreadyJoined = docEventHandlers.has(did)
const result = await overleafSock.joinDoc(did)
if (result.ranges?.comments) {
for (const c of result.ranges.comments) {
if (c.op?.t) contexts[c.op.t] = { file: rp, text: c.op.c || '', pos: c.op.p || 0 }
}
}
- if (!alreadyJoined) await overleafSock.leaveDoc(did)
+ // Don't leaveDoc — bridge keeps all docs joined
} catch { /* ignore */ }
}
mcpCommentContexts = contexts