summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-14 22:26:36 -0500
committerhaoyuren <13851610112@163.com>2026-03-14 22:26:36 -0500
commitc261b8c4a95a7af64e3cd95a65c50f4dcbbb802c (patch)
treeb2b31cd96a1ab8b1740402b417692a7ef5935581 /src/renderer
parent2ee6d867bd93bb955429a274865320dfa5bd0f69 (diff)
Add new file sync to Overleaf, Copy Comments button with file filtering
- Sync new local files (created by Claude Code etc.) to Overleaf via REST API - Create intermediate folders as needed, handle both text docs and binaries - Scan for orphaned files on startup that weren't synced previously - Add "Copy Comments" quick action: copies unresolved comments for current file to clipboard with line numbers, context, author names, and timestamps - Filter comments to active file only, exclude resolved threads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/src/App.tsx8
-rw-r--r--src/renderer/src/components/Terminal.tsx75
-rw-r--r--src/renderer/src/stores/appStore.ts5
3 files changed, 88 insertions, 0 deletions
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 869bdae..82306c9 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -104,6 +104,13 @@ export default function App() {
if (sync) sync.replaceContent(data.content)
})
+ // Listen for new docs created locally (e.g. by Claude Code)
+ const unsubNewDoc = window.api.onSyncNewDoc((data) => {
+ if (data.docId) {
+ useAppStore.getState().addDocPath(data.docId, data.relPath)
+ }
+ })
+
// Listen for remote cursor updates
const unsubCursorUpdate = window.api.onCursorRemoteUpdate((raw) => {
const data = raw as {
@@ -160,6 +167,7 @@ export default function App() {
unsubState()
unsubRejoined()
unsubExternalEdit()
+ unsubNewDoc()
unsubCursorUpdate()
unsubCursorDisconnected()
remoteCursors.clear()
diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx
index 72f785f..bc29633 100644
--- a/src/renderer/src/components/Terminal.tsx
+++ b/src/renderer/src/components/Terminal.tsx
@@ -168,6 +168,77 @@ function QuickActions({ ptyId }: { ptyId: string }) {
window.api.ptyWrite(ptyId, prompt + '\n')
}
+ const copyComments = async () => {
+ const store = useAppStore.getState()
+ const projectId = store.overleafProjectId
+ if (!projectId) return
+
+ // Fetch threads and contexts in parallel
+ const [threadResult, ctxResult] = await Promise.all([
+ window.api.overleafGetThreads(projectId),
+ window.api.otFetchAllCommentContexts()
+ ])
+
+ const threads = (threadResult.success ? threadResult.threads : {}) as Record<string, {
+ messages: Array<{ content: string; timestamp?: number; user?: { first_name?: string; last_name?: string } }>
+ resolved?: boolean
+ }>
+ const contexts = ctxResult.success && ctxResult.contexts ? ctxResult.contexts : store.commentContexts
+
+ const lines: string[] = []
+ for (const [threadId, thread] of Object.entries(threads)) {
+ if (thread.resolved) continue
+ const ctx = contexts[threadId]
+ if (!ctx) continue
+ if (ctx.file !== activeTab) continue
+
+ const firstMsg = thread.messages?.[0]
+ if (!firstMsg) continue
+
+ // Compute line number and surrounding context from file content
+ const content = store.fileContents[ctx.file] || ''
+ let lineNum = 0
+ let contextSnippet = ctx.text
+ if (content) {
+ const before = content.slice(0, ctx.pos)
+ lineNum = (before.match(/\n/g) || []).length + 1
+ // Get the full line(s) containing the comment for context
+ const lineStart = before.lastIndexOf('\n') + 1
+ const afterComment = content.indexOf('\n', ctx.pos + ctx.text.length)
+ const lineEnd = afterComment === -1 ? content.length : afterComment
+ const fullLine = content.slice(lineStart, lineEnd).trim()
+ // Show full line with the commented text marked
+ if (fullLine.length > ctx.text.length) {
+ contextSnippet = fullLine.replace(ctx.text, `«${ctx.text}»`)
+ }
+ }
+
+ const fmtTime = (ts?: number) => ts ? new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : ''
+ const author = firstMsg.user ? [firstMsg.user.first_name, firstMsg.user.last_name].filter(Boolean).join(' ') : ''
+ const time = fmtTime(firstMsg.timestamp)
+ const attribution = [author, time].filter(Boolean).join(', ')
+ let line = `- ${ctx.file}:${lineNum}: ${contextSnippet}\n → "${firstMsg.content}"${attribution ? ` — ${attribution}` : ''}`
+
+ // Add replies
+ for (let i = 1; i < thread.messages.length; i++) {
+ const reply = thread.messages[i]
+ const rAuthor = reply.user ? [reply.user.first_name, reply.user.last_name].filter(Boolean).join(' ') : ''
+ const rTime = fmtTime(reply.timestamp)
+ const rAttribution = [rAuthor, rTime].filter(Boolean).join(', ')
+ line += `\n → "${reply.content}"${rAttribution ? ` — ${rAttribution}` : ''}`
+ }
+ lines.push(line)
+ }
+
+ if (lines.length === 0) {
+ navigator.clipboard.writeText('No unresolved comments.')
+ return
+ }
+
+ const text = `Overleaf comments (${lines.length} unresolved):\n\n${lines.join('\n\n')}`
+ navigator.clipboard.writeText(text)
+ }
+
const actions = [
{
label: 'Fix Errors',
@@ -193,6 +264,10 @@ function QuickActions({ ptyId }: { ptyId: string }) {
send(`Explain the structure and content of: ${activeTab}`)
}
}
+ },
+ {
+ label: 'Copy Comments',
+ action: copyComments
}
]
diff --git a/src/renderer/src/stores/appStore.ts b/src/renderer/src/stores/appStore.ts
index adb4957..e9c47ed 100644
--- a/src/renderer/src/stores/appStore.ts
+++ b/src/renderer/src/stores/appStore.ts
@@ -82,6 +82,7 @@ interface AppState {
docPathMap: Record<string, string> // docId → relativePath
pathDocMap: Record<string, string> // relativePath → docId
setDocMaps: (docPath: Record<string, string>, pathDoc: Record<string, string>) => void
+ addDocPath: (docId: string, relPath: string) => void
docVersions: Record<string, number> // docId → version
setDocVersion: (docId: string, version: number) => void
overleafProject: { name: string; rootDocId: string } | null
@@ -191,6 +192,10 @@ export const useAppStore = create<AppState>((set) => ({
docPathMap: {},
pathDocMap: {},
setDocMaps: (docPath, pathDoc) => set({ docPathMap: docPath, pathDocMap: pathDoc }),
+ addDocPath: (docId, relPath) => set((s) => ({
+ docPathMap: { ...s.docPathMap, [docId]: relPath },
+ pathDocMap: { ...s.pathDocMap, [relPath]: docId }
+ })),
docVersions: {},
setDocVersion: (docId, version) =>
set((s) => ({ docVersions: { ...s.docVersions, [docId]: version } })),