From d0d39fa28bfa504d3a5d55f30f476cf78f528b7c Mon Sep 17 00:00:00 2001 From: HUQIANTAO Date: Tue, 23 Jun 2026 12:50:40 +0800 Subject: [PATCH 1/2] fix(desktop): close tabs when removing workspace to prevent ghost reappear When a user removes a workspace from the sidebar, RemoveWorkspace only cleared desktop-projects.json and desktop-workspaces.json but left open tabs referencing that workspace in desktop-tabs.json. On shutdown the tab state was persisted; on the next startup restoreOrBuildTabs recreated those tabs and ensureTopicIndexed unconditionally re-added the project to desktop-projects.json, causing the removed workspace to reappear. Close all tabs belonging to the workspace before removing it from the project index, following the same pattern as TrashTopic. If all tabs are removed, create a fallback global tab so the app is not left headless. Fixes #5079 --- desktop/app.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/desktop/app.go b/desktop/app.go index 8442f3c044..01ba493403 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -1123,6 +1123,60 @@ func (a *App) RemoveWorkspace(dir string) error { return fmt.Errorf("workspace path is required") } dir = normalizeProjectRoot(dir) + + // Close all tabs belonging to this workspace so that saveTabsLocked does + // not persist them. On the next startup restoreOrBuildTabs would otherwise + // re-create the tabs and ensureTopicIndexed would re-add the project to + // desktop-projects.json, making the removed workspace reappear. + a.mu.RLock() + var workspaceTabs []*WorkspaceTab + for _, tab := range a.tabs { + if tab.WorkspaceRoot == dir { + workspaceTabs = append(workspaceTabs, tab) + } + } + a.mu.RUnlock() + + for _, tab := range workspaceTabs { + if tab.Ctrl != nil { + tab.Ctrl.Snapshot() + tab.Ctrl.Close() + } + if tab.sink != nil { + tab.sink.ctx = nil + } + } + + if len(workspaceTabs) > 0 { + a.mu.Lock() + removedActive := false + for _, tab := range workspaceTabs { + if a.tabs[tab.ID] != tab { + continue + } + if a.activeTabID == tab.ID { + removedActive = true + } + delete(a.tabs, tab.ID) + a.removeTabOrderLocked(tab.ID) + } + if removedActive { + a.activeTabID = "" + if len(a.tabOrder) > 0 { + a.activeTabID = a.tabOrder[0] + } + } + // If all tabs were removed, create a fallback global tab so the app + // is not left in a headless state. + if len(a.tabs) == 0 { + a.mu.Unlock() + a.OpenGlobalTab("") + a.mu.Lock() + } + a.saveTabsLocked() + a.mu.Unlock() + } + forgetWorkspace(dir) if err := removeProject(dir); err != nil { return err From d4fb0b45b648da91612065433f36850868fed260 Mon Sep 17 00:00:00 2001 From: HUQIANTAO Date: Tue, 23 Jun 2026 13:22:20 +0800 Subject: [PATCH 2/2] fix: check error returns from Snapshot and OpenGlobalTab --- desktop/app.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/desktop/app.go b/desktop/app.go index 01ba493403..450455f1d1 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -1139,7 +1139,9 @@ func (a *App) RemoveWorkspace(dir string) error { for _, tab := range workspaceTabs { if tab.Ctrl != nil { - tab.Ctrl.Snapshot() + if err := tab.Ctrl.Snapshot(); err != nil { + return err + } tab.Ctrl.Close() } if tab.sink != nil { @@ -1170,7 +1172,9 @@ func (a *App) RemoveWorkspace(dir string) error { // is not left in a headless state. if len(a.tabs) == 0 { a.mu.Unlock() - a.OpenGlobalTab("") + if _, err := a.OpenGlobalTab(""); err != nil { + return err + } a.mu.Lock() } a.saveTabsLocked()