diff options
| author | haoyuren <13851610112@163.com> | 2026-08-18 20:24:40 +0800 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-08-18 20:24:40 +0800 |
| commit | ec8214a4d111d92ce7c44de29d1706f241aa9514 (patch) | |
| tree | d5ffce9791bb94b4a4336ae26d091ba2e010d364 /src/main/fileSyncBridge.ts | |
| parent | df3092328865949c088eed321e6675e3b7758c36 (diff) | |
Add browser-style project tabs: persistent home + one tab per projectv0.6.0
The project list is now a persistent home tab; each opened project runs
in its own WebContentsView tab below a 38px tab strip (hidden when only
home is open). Background tabs stay fully live (sync, collab, terminals).
Main process: per-project singletons (socket, sync bridge, compilation
manager, MCP state, compile watcher) become per-tab ProjectSession
objects keyed by webContents.id; PTYs are scoped per webContents.
Hardened against races found in review: concurrent ot:connect dedupe,
teardown/reopen serialization on the shared sync dir, destroyed-window
guards on all bridge sends, real logout that closes project tabs.
Renderer: tabs strip in the home renderer (wt-* classes, distinct from
the editor's .tab-bar file tabs), project tabs boot from ?projectId= and
connect straight into the editor; Back closes the tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'src/main/fileSyncBridge.ts')
| -rw-r--r-- | src/main/fileSyncBridge.ts | 32 |
1 files changed, 21 insertions, 11 deletions
diff --git a/src/main/fileSyncBridge.ts b/src/main/fileSyncBridge.ts index ae22eb8..08bf69e 100644 --- a/src/main/fileSyncBridge.ts +++ b/src/main/fileSyncBridge.ts @@ -10,7 +10,7 @@ import { createHash } from 'crypto' import * as chokidar from 'chokidar' import { diff_match_patch } from 'diff-match-patch' import { net } from 'electron' -import type { BrowserWindow } from 'electron' +import type { WebContents } from 'electron' import type { OverleafSocket } from './overleafSocket' import { OtClient } from './otClient' import type { OtOp } from './otTypes' @@ -87,7 +87,7 @@ export class FileSyncBridge { private pathFileRefMap: Record<string, string> // relPath → fileRefId private folderPathMap: Record<string, string> // folderId → relDirPath (no trailing slash) private pathFolderMap: Record<string, string> // relDirPath (no trailing slash) → folderId - private mainWindow: BrowserWindow + private target: WebContents // the owning tab's webContents private projectId: string private cookie: string private csrfToken: string @@ -103,7 +103,7 @@ export class FileSyncBridge { docPathMap: Record<string, string>, pathDocMap: Record<string, string>, fileRefs: Array<{ id: string; path: string }>, - mainWindow: BrowserWindow, + target: WebContents, projectId: string, cookie: string, csrfToken: string, @@ -114,7 +114,7 @@ export class FileSyncBridge { this.tmpDir = tmpDir this.docPathMap = docPathMap this.pathDocMap = pathDocMap - this.mainWindow = mainWindow + this.target = target this.projectId = projectId this.cookie = cookie this.csrfToken = csrfToken @@ -818,7 +818,7 @@ export class FileSyncBridge { // lastKnownContent go stale or we'll re-detect the same "change" indefinitely. this.lastKnownContent.set(relPath, newContent) bridgeLog(`[FileSyncBridge] → sending sync:externalEdit to renderer for ${relPath}`) - this.mainWindow.webContents.send('sync:externalEdit', { docId, content: newContent, baseContent: lastKnown ?? '' }) + this.sendToWindow('sync:externalEdit', { docId, content: newContent, baseContent: lastKnown ?? '' }) } else { // Doc NOT open in editor → bridge handles OT directly const oldContent = lastKnown ?? '' @@ -1168,7 +1168,7 @@ export class FileSyncBridge { name: string, parentFolderId?: string ): void { - this.mainWindow.webContents.send('sync:entityCreated', { + this.sendToWindow('sync:entityCreated', { kind, entityId, relPath, @@ -1178,7 +1178,7 @@ export class FileSyncBridge { } private notifyEntityRemoved(kind: 'doc' | 'file' | 'folder', entityId: string, relPath: string): void { - this.mainWindow.webContents.send('sync:entityRemoved', { kind, entityId, relPath }) + this.sendToWindow('sync:entityRemoved', { kind, entityId, relPath }) } private notifyEntityRenamed( @@ -1188,7 +1188,7 @@ export class FileSyncBridge { newPath: string, newName: string ): void { - this.mainWindow.webContents.send('sync:entityRenamed', { + this.sendToWindow('sync:entityRenamed', { kind, entityId, oldPath, @@ -1204,7 +1204,7 @@ export class FileSyncBridge { newPath: string, parentFolderId: string ): void { - this.mainWindow.webContents.send('sync:entityMoved', { + this.sendToWindow('sync:entityMoved', { kind, entityId, oldPath, @@ -1584,8 +1584,18 @@ export class FileSyncBridge { } private notifySyncStatus(relPath: string, status: 'retrying' | 'synced' | 'failed', attempts?: number): void { - if (this.mainWindow.isDestroyed() || this.mainWindow.webContents.isDestroyed()) return - this.mainWindow.webContents.send('sync:fileStatus', { relPath, status, attempts }) + this.sendToWindow('sync:fileStatus', { relPath, status, attempts }) + } + + /** + * Send to the owning tab, guarded — the bridge can outlive its webContents + * briefly (a debounced disk change firing during tab close), and an + * unguarded send to destroyed webContents throws inside an unawaited async + * path, which would take down the whole main process. + */ + private sendToWindow(channel: string, payload: unknown): void { + if (this.target.isDestroyed()) return + this.target.send(channel, payload) } /** Create a text doc on Overleaf and sync its content */ |
