From ec8214a4d111d92ce7c44de29d1706f241aa9514 Mon Sep 17 00:00:00 2001 From: haoyuren <13851610112@163.com> Date: Tue, 18 Aug 2026 20:24:40 +0800 Subject: Add browser-style project tabs: persistent home + one tab per project 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 --- src/preload/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/preload/index.ts') diff --git a/src/preload/index.ts b/src/preload/index.ts index a4d8179..ee9fd9b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -63,6 +63,7 @@ const api = { // Overleaf web session (comments) overleafWebLogin: () => ipcRenderer.invoke('overleaf:webLogin') as Promise<{ success: boolean }>, overleafHasWebSession: () => ipcRenderer.invoke('overleaf:hasWebSession') as Promise<{ loggedIn: boolean }>, + overleafLogout: () => ipcRenderer.invoke('overleaf:logout') as Promise, overleafGetThreads: (projectId: string) => ipcRenderer.invoke('overleaf:getThreads', projectId) as Promise<{ success: boolean; threads?: Record; message?: string }>, overleafReplyThread: (projectId: string, threadId: string, content: string) => @@ -370,6 +371,20 @@ const api = { getApiKeys: () => ipcRenderer.invoke('settings:getApiKeys') as Promise>, setApiKeys: (keys: Record) => ipcRenderer.invoke('settings:setApiKeys', keys), + // Project tabs (browser-tab model: home = project list, one tab per project) + openProjectTab: (projectId: string, name?: string) => + ipcRenderer.invoke('project:openTab', projectId, name) as Promise<{ success: boolean; focusedExisting?: boolean }>, + tabsList: () => + ipcRenderer.invoke('tabs:list') as Promise<{ tabs: Array<{ id: string; title: string }>; active: string }>, + tabsActivate: (id: string) => ipcRenderer.invoke('tabs:activate', id) as Promise, + tabsClose: (id: string) => ipcRenderer.invoke('tabs:close', id) as Promise, + onTabsChanged: (cb: (state: { tabs: Array<{ id: string; title: string }>; active: string }) => void) => { + const handler = (_e: Electron.IpcRendererEvent, state: { tabs: Array<{ id: string; title: string }>; active: string }) => cb(state) + ipcRenderer.on('tabs:changed', handler) + return () => { ipcRenderer.removeListener('tabs:changed', handler) } + }, + closeWindow: () => ipcRenderer.invoke('window:close') as Promise, + // Shell openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url), showInFinder: (path: string) => ipcRenderer.invoke('shell:showInFinder', path), -- cgit v1.2.3