From 183af193dcf46838506958a50daad61c6b29a23d Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Sun, 15 Mar 2026 04:00:12 -0500 Subject: Fix server compile: download PDF to .build dir, prevent artifact sync to Overleaf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause of server compile failures was that output.pdf was being saved into the synced project directory, causing FileSyncBridge to upload it back to Overleaf as a project file. CLSI then failed because it found an existing output.pdf blocking its compilation output. Changes: - Save compile artifacts (PDF, synctex.gz) to .build/ subdirectory instead of the synced project root — .build is a dotfile dir ignored by chokidar - Add pdf/pdfxref/stderr/stdout/chktex to FileSyncBridge ignore patterns - Add rootResourcePath to compile request body (matches Overleaf web client) - Implement PDF download with fallback via direct build ID URL construction - Add server compile handler, compile dropdown menu, PDF save button - Fix resolved comment highlight flash on startup (null initial state) - Fix EPIPE crash on startup when stdout/stderr is closed - Fix synctex inverse search to use relative paths via OT doc join Co-Authored-By: Claude Opus 4.6 --- src/renderer/src/App.css | 55 ++++++++++++++++++++++++++++ src/renderer/src/App.tsx | 30 +++++++++++++-- src/renderer/src/components/Editor.tsx | 3 +- src/renderer/src/components/PdfViewer.tsx | 38 +++++++++++++++---- src/renderer/src/components/ReviewPanel.tsx | 12 +++--- src/renderer/src/components/Toolbar.tsx | 57 ++++++++++++++++++++++++----- src/renderer/src/stores/appStore.ts | 8 ++-- 7 files changed, 172 insertions(+), 31 deletions(-) (limited to 'src/renderer') diff --git a/src/renderer/src/App.css b/src/renderer/src/App.css index a6a4003..4c3f731 100644 --- a/src/renderer/src/App.css +++ b/src/renderer/src/App.css @@ -263,6 +263,61 @@ html, body, #root { 50% { opacity: 0.6; } } +.compile-btn-group { + display: flex; + position: relative; +} + +.compile-btn-group .toolbar-btn-primary:first-child { + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + border-right: 1px solid rgba(255,255,255,0.2); +} + +.compile-dropdown-toggle { + border-radius: 0 var(--radius-sm) var(--radius-sm) 0 !important; + padding: 0 4px !important; + min-width: 20px; + font-size: 9px; +} + +.compile-dropdown-menu { + position: absolute; + top: 100%; + left: 0; + margin-top: 4px; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 100; + min-width: 160px; + overflow: hidden; +} + +.compile-dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 6px 12px; + border: none; + background: none; + color: var(--text-primary); + font-size: 12px; + cursor: pointer; + text-align: left; +} + +.compile-dropdown-item:hover { + background: var(--bg-hover); +} + +.compile-dropdown-hint { + font-size: 10px; + color: var(--text-muted); + margin-left: 12px; +} + .toolbar-main-doc { font-size: 11px; color: var(--text-muted); diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ca2fc1e..6992a17 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -213,6 +213,29 @@ export default function App() { }, [screen]) const handleCompile = async () => { + const state = useAppStore.getState() + const mainDoc = state.mainDocument || state.overleafProject?.rootDocId + if (!mainDoc) { + setStatusMessage('No main document set') + return + } + state.setCompiling(true) + state.clearCompileLog() + setStatusMessage('Compiling on server...') + + const result = await window.api.overleafServerCompile(mainDoc) + + if (result.log) { + useAppStore.getState().appendCompileLog(result.log) + } + if (result.pdfPath) { + useAppStore.getState().setPdfPath(result.pdfPath) + } + useAppStore.getState().setCompiling(false) + setStatusMessage(result.success ? 'Compiled successfully' : 'Compilation had errors — check Log tab') + } + + const handleLocalCompile = async () => { const state = useAppStore.getState() const mainDoc = state.mainDocument || state.overleafProject?.rootDocId if (!mainDoc) { @@ -222,12 +245,11 @@ export default function App() { const relPath = state.docPathMap[mainDoc] || mainDoc state.setCompiling(true) state.clearCompileLog() - setStatusMessage('Compiling...') + setStatusMessage('Compiling locally...') const result = await window.api.overleafSocketCompile(relPath) - const storeLog = useAppStore.getState().compileLog - if (!storeLog && result.log) { + if (!useAppStore.getState().compileLog && result.log) { useAppStore.getState().appendCompileLog(result.log) } if (result.pdfPath) { @@ -358,7 +380,7 @@ export default function App() {
- +
{showFileTree && ( diff --git a/src/renderer/src/components/Editor.tsx b/src/renderer/src/components/Editor.tsx index 4252464..4edb945 100644 --- a/src/renderer/src/components/Editor.tsx +++ b/src/renderer/src/components/Editor.tsx @@ -335,8 +335,9 @@ export default function Editor() { }, [activeTab, pathDocMap]) // Sync comment ranges to CodeMirror (exclude resolved threads) + // Skip until resolvedThreadIds has been loaded (non-null) to avoid flashing resolved highlights useEffect(() => { - if (!viewRef.current || !activeTab) return + if (!viewRef.current || !activeTab || resolvedThreadIds === null) return const ranges: CommentRange[] = [] for (const [threadId, ctx] of Object.entries(commentContexts)) { if (ctx.file === activeTab && ctx.text && !resolvedThreadIds.has(threadId)) { diff --git a/src/renderer/src/components/PdfViewer.tsx b/src/renderer/src/components/PdfViewer.tsx index 1e1fd7c..c5fe8c4 100644 --- a/src/renderer/src/components/PdfViewer.tsx +++ b/src/renderer/src/components/PdfViewer.tsx @@ -241,13 +241,32 @@ export default function PdfViewer() { const result = await window.api.synctexEdit(pdfPath, pageNum, pdfX, pdfY) if (!result) return - // Navigate to the source file:line - try { - const content = await window.api.readFile(result.file) - useAppStore.getState().setFileContent(result.file, content) - useAppStore.getState().openFile(result.file, result.file.split('/').pop() || result.file) - useAppStore.getState().setPendingGoTo({ file: result.file, line: result.line }) - } catch { /* file not found */ } + // Navigate to source — synctex returns relative path (e.g. "latex/main.tex") + const store = useAppStore.getState() + const relPath = result.file + + // If already loaded in editor, just navigate + if (store.fileContents[relPath]) { + store.openFile(relPath, relPath.split('/').pop() || relPath) + store.setPendingGoTo({ file: relPath, line: result.line }) + return + } + + // Not loaded — join via socket + const docId = store.pathDocMap[relPath] + if (docId) { + try { + const joinResult = await window.api.otJoinDoc(docId) + if (joinResult.success && joinResult.content !== undefined) { + useAppStore.getState().setFileContent(relPath, joinResult.content) + if (joinResult.version !== undefined) { + useAppStore.getState().setDocVersion(docId, joinResult.version) + } + useAppStore.getState().openFile(relPath, relPath.split('/').pop() || relPath) + useAppStore.getState().setPendingGoTo({ file: relPath, line: result.line }) + } + } catch { /* failed to join doc */ } + } }, [pdfPath]) // Render PDF (with lock to prevent double-render) @@ -356,6 +375,11 @@ export default function PdfViewer() { {Math.round(scale * 100)}% + {pdfPath && ( + + )} )} {tab === 'log' && ( diff --git a/src/renderer/src/components/ReviewPanel.tsx b/src/renderer/src/components/ReviewPanel.tsx index 7e6c9e5..9396ddf 100644 --- a/src/renderer/src/components/ReviewPanel.tsx +++ b/src/renderer/src/components/ReviewPanel.tsx @@ -123,7 +123,7 @@ export default function ReviewPanel() { } }) const store = useAppStore.getState() - store.setResolvedThreadIds(new Set([...store.resolvedThreadIds, threadId])) + store.setResolvedThreadIds(new Set([...(store.resolvedThreadIds || []), threadId])) break } case 'reopen-thread': { @@ -137,7 +137,7 @@ export default function ReviewPanel() { return { ...prev, [threadId]: t } }) const store = useAppStore.getState() - const ids = new Set(store.resolvedThreadIds) + const ids = new Set(store.resolvedThreadIds || []) ids.delete(threadId) store.setResolvedThreadIds(ids) break @@ -154,7 +154,7 @@ export default function ReviewPanel() { const newCtx = { ...store.commentContexts } delete newCtx[threadId] store.setCommentContexts(newCtx) - const ids = new Set(store.resolvedThreadIds) + const ids = new Set(store.resolvedThreadIds || []) ids.delete(threadId) store.setResolvedThreadIds(ids) break @@ -228,7 +228,7 @@ export default function ReviewPanel() { return { ...prev, [threadId]: { ...prev[threadId], resolved: true, resolved_at: new Date().toISOString() } } }) const store = useAppStore.getState() - store.setResolvedThreadIds(new Set([...store.resolvedThreadIds, threadId])) + store.setResolvedThreadIds(new Set([...(store.resolvedThreadIds || []), threadId])) await window.api.overleafResolveThread(overleafProjectId, threadId, getDocIdForThread(threadId)) } @@ -244,7 +244,7 @@ export default function ReviewPanel() { return { ...prev, [threadId]: t } }) const store = useAppStore.getState() - const ids = new Set(store.resolvedThreadIds) + const ids = new Set(store.resolvedThreadIds || []) ids.delete(threadId) store.setResolvedThreadIds(ids) await window.api.overleafReopenThread(overleafProjectId, threadId, getDocIdForThread(threadId)) @@ -307,7 +307,7 @@ export default function ReviewPanel() { const newCtx = { ...store.commentContexts } delete newCtx[threadId] store.setCommentContexts(newCtx) - const ids = new Set(store.resolvedThreadIds) + const ids = new Set(store.resolvedThreadIds || []) ids.delete(threadId) store.setResolvedThreadIds(ids) diff --git a/src/renderer/src/components/Toolbar.tsx b/src/renderer/src/components/Toolbar.tsx index b480ede..94ddb5e 100644 --- a/src/renderer/src/components/Toolbar.tsx +++ b/src/renderer/src/components/Toolbar.tsx @@ -1,20 +1,37 @@ // Copyright (c) 2026 Yuren Hao // Licensed under AGPL-3.0 - see LICENSE file +import { useState, useRef, useEffect } from 'react' import { useAppStore } from '../stores/appStore' interface ToolbarProps { onCompile: () => void + onLocalCompile: () => void onBack: () => void } -export default function Toolbar({ onCompile, onBack }: ToolbarProps) { +export default function Toolbar({ onCompile, onLocalCompile, onBack }: ToolbarProps) { const { compiling, toggleTerminal, toggleFileTree, showTerminal, showFileTree, showReviewPanel, toggleReviewPanel, showChat, toggleChat, connectionState, overleafProject, onlineUsersCount } = useAppStore() + const [showCompileMenu, setShowCompileMenu] = useState(false) + const menuRef = useRef(null) + + // Close menu on outside click + useEffect(() => { + if (!showCompileMenu) return + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setShowCompileMenu(false) + } + } + document.addEventListener('mousedown', handler) + return () => document.removeEventListener('mousedown', handler) + }, [showCompileMenu]) + const projectName = overleafProject?.name || 'Project' const connectionDot = connectionState === 'connected' ? 'connection-dot-green' @@ -37,14 +54,36 @@ export default function Toolbar({ onCompile, onBack }: ToolbarProps) {
- +
+ + + {showCompileMenu && ( +
+ + +
+ )} +
{onlineUsersCount > 0 && ( diff --git a/src/renderer/src/stores/appStore.ts b/src/renderer/src/stores/appStore.ts index ff56c28..9a1d441 100644 --- a/src/renderer/src/stores/appStore.ts +++ b/src/renderer/src/stores/appStore.ts @@ -109,8 +109,8 @@ interface AppState { // Comment data commentContexts: Record setCommentContexts: (c: Record) => void - resolvedThreadIds: Set - setResolvedThreadIds: (ids: Set) => void + resolvedThreadIds: Set | null + setResolvedThreadIds: (ids: Set | null) => void overleafDocs: Record setOverleafDocs: (d: Record) => void hoveredThreadId: string | null @@ -221,7 +221,7 @@ export const useAppStore = create((set) => ({ commentContexts: {}, setCommentContexts: (c) => set({ commentContexts: c }), - resolvedThreadIds: new Set(), + resolvedThreadIds: null, setResolvedThreadIds: (ids) => set({ resolvedThreadIds: ids }), overleafDocs: {}, setOverleafDocs: (d) => set({ overleafDocs: d }), @@ -255,7 +255,7 @@ export const useAppStore = create((set) => ({ rootFolderId: '', syncDir: '', commentContexts: {}, - resolvedThreadIds: new Set(), + resolvedThreadIds: null, overleafDocs: {}, hoveredThreadId: null, focusedThreadId: null, -- cgit v1.2.3