diff options
| author | haoyuren <13851610112@163.com> | 2026-03-18 08:06:32 +0000 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-18 08:06:32 +0000 |
| commit | 9b5256718c2117511f0253a656bb8cff7410b92a (patch) | |
| tree | 8ba0fd257f771538874f37b87dcaeb5471185ca5 /src/main/index.ts | |
| parent | 69a09baf71798966724d942b93303211516e34c7 (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.ts | 32 |
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 |
