Skip to content

fix(desktop): close tabs when removing workspace to prevent ghost reappear#5121

Open
HUQIANTAO wants to merge 2 commits into
esengine:main-v2from
HUQIANTAO:fix/workspace-removal-persistence
Open

fix(desktop): close tabs when removing workspace to prevent ghost reappear#5121
HUQIANTAO wants to merge 2 commits into
esengine:main-v2from
HUQIANTAO:fix/workspace-removal-persistence

Conversation

@HUQIANTAO

Copy link
Copy Markdown
Contributor

Problem

Removed workspaces reappear in the sidebar every time the application restarts.

Reproduction (Issue #5079):

  1. Open a project workspace in a tab
  2. Right-click the project in the sidebar and select "Remove from sidebar"
  3. Close and relaunch Reasonix
  4. The removed project reappears in the sidebar

Root Cause

Three persistence layers are involved:

File Purpose
desktop-projects.json Project list with topics
desktop-workspaces.json Legacy workspace list
desktop-tabs.json Open tabs with workspaceRoot and topicId

RemoveWorkspace (desktop/app.go:1121) only cleaned up the first two:

forgetWorkspace(dir)       → removes from desktop-workspaces.json
removeProject(dir)         → removes from desktop-projects.json
// desktop-tabs.json is NOT cleaned up

On shutdown, saveTabsLocked() persisted the still-open tabs (with their WorkspaceRoot pointing to the removed workspace) back to desktop-tabs.json. On the next startup:

  1. restoreOrBuildTabs reads desktop-tabs.json and creates tabs for the removed workspace
  2. buildTabController calls ensureTopicIndexed (desktop/tabs.go:1364)
  3. ensureTopicIndexed unconditionally re-adds the project to desktop-projects.json if not present
  4. The removed workspace reappears in the sidebar

Fix

Close all tabs belonging to the workspace before removing it from the project index. This follows the same pattern used by TrashTopic (desktop/tabs.go:1937):

  1. Collect all tabs where WorkspaceRoot == dir
  2. Snapshot and close each tab's controller
  3. Remove tabs from a.tabs and a.tabOrder
  4. Handle active tab selection if the removed tab was active
  5. Create a fallback global tab if all tabs were removed
  6. Call saveTabsLocked() to persist the cleaned state

Then proceed with the existing forgetWorkspace + removeProject cleanup.

Changes in desktop/app.go:

func (a *App) RemoveWorkspace(dir string) error {
    if dir == "" {
        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 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
    }
    a.emitProjectTreeChanged()
    return nil
}

Testing

  1. Open a project workspace in one or more tabs
  2. Remove the workspace from the sidebar
  3. Verify all tabs for that workspace are closed
  4. Verify a fallback tab is created if no tabs remain
  5. Restart the application
  6. Verify the removed workspace does not reappear in the sidebar
  7. Verify remaining workspaces and their tabs are unaffected

…ppear

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 esengine#5079
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant