返回 DeepSeek-Reasonix
tab_prune.go
根目录 / desktop / tab_prune.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7 )
8
9 func (a *App) keepOnlyVisibleTab(tabID string) (TabMeta, error) {
10 meta, err := func() (TabMeta, error) {
11 defer a.lockRuntimeMutation("prune-visible-tabs")()
12 return a.pruneVisibleTabsRuntimeAdmissionHeld(tabID)
13 }()
14 if err != nil {
15 return TabMeta{}, err
16 }
17 // Visibility and detached/open ownership are runtime state. Snapshot saves
18 // already enqueue exact-path catalog updates; switching must not rescan.
19 a.emitProjectTreeRuntimeChangedWithLegacy()
20 return enrichTabMeta(meta), nil
21 }
22
23 // The caller holds the runtime mutation barrier. The removal lock covers
24 // snapshots, bindings and teardown; events must be emitted after these locks
25 // are released so a listener cannot re-enter the removal path.
26 func (a *App) pruneVisibleTabsRuntimeAdmissionHeld(tabID string) (TabMeta, error) {
27 type pruneCandidate struct {
28 id string
29 tab *WorkspaceTab
30 }
31 a.sessionRemovalMu.Lock()
32 defer a.sessionRemovalMu.Unlock()
33
34 a.mu.Lock()
35 active := a.tabs[tabID]
36 if active == nil {
37 a.mu.Unlock()
38 return TabMeta{}, fmt.Errorf("tab %q not found", tabID)
39 }
40 candidates := make([]pruneCandidate, 0, len(a.tabs)-1)
41 for id, tab := range a.tabs {
42 if id == tabID {
43 continue
44 }
45 candidates = append(candidates, pruneCandidate{id: id, tab: tab})
46 }
47 a.mu.Unlock()
48
49 // Keep bindings visible while saving. Snapshot may invoke recovery
50 // callbacks that need App.mu, so it must run outside that lock.
51 snapshotted := make(map[string]*WorkspaceTab, len(candidates))
52 for _, candidate := range candidates {
53 id, tab := candidate.id, candidate.tab
54 snapshotted[id] = tab
55 if err := a.persistHiddenTabBeforePrune(id, tab); err != nil {
56 return TabMeta{}, err
57 }
58 }
59
60 a.mu.Lock()
61 active = a.tabs[tabID]
62 if active == nil {
63 a.mu.Unlock()
64 return TabMeta{}, fmt.Errorf("tab %q not found", tabID)
65 }
66 for id, tab := range a.tabs {
67 if id != tabID && snapshotted[id] != tab {
68 a.mu.Unlock()
69 return TabMeta{}, fmt.Errorf("visible tabs changed while switching; retry")
70 }
71 }
72 a.activeTabID = tabID
73 removed := make([]*WorkspaceTab, 0, len(candidates))
74 for _, candidate := range candidates {
75 id, tab := candidate.id, candidate.tab
76 if tab == nil || a.tabs[id] != tab {
77 continue
78 }
79 if tab.Ctrl == nil || !tab.hasActiveRuntimeWork() {
80 a.markTabRemovedLocked(tab)
81 }
82 removed = append(removed, tab)
83 delete(a.tabs, id)
84 a.removeTabOrderLocked(id)
85 }
86 a.tabOrder = []string{tabID}
87 // A local topic replaces the remote visible surface too. Leaving its
88 // registry entry active makes ListTabs hide the newly selected local tab.
89 a.remoteTabMu.Lock()
90 remoteCancels := make([]context.CancelFunc, 0, len(a.remoteTabs))
91 for id, remote := range a.remoteTabs {
92 if remote.cancel != nil {
93 remoteCancels = append(remoteCancels, remote.cancel)
94 }
95 delete(a.remoteTabs, id)
96 a.forgetRemoteBrowserExecutor(id)
97 }
98 a.remoteTabLayout = remoteTabLayoutState{}
99 a.remoteTabMu.Unlock()
100 a.saveTabsLocked()
101 meta := a.tabMeta(active, true)
102 a.mu.Unlock()
103
104 for _, cancel := range remoteCancels {
105 cancel()
106 }
107 for _, tab := range removed {
108 a.removeVisibleTabRuntimeAdmissionHeld(tab)
109 }
110 return meta, nil
111 }
112
113 // Async cleanup must not hold singleSurfaceMu while waiting for runtime work:
114 // that would prevent later clicks from publishing their readable identities.
115 // Existing synchronous callers lock the surface first, so never block on that
116 // lock while holding the runtime barrier. Release and wait outside both locks
117 // if a synchronous caller owns the surface. On return the caller owns both;
118 // it must release singleSurfaceMu and may release runtime admission early.
119 func (a *App) lockTopicActivationPrune() func() {
120 for {
121 unlockRuntime := a.lockRuntimeMutation("prune-visible-tabs")
122 if a.singleSurfaceMu.TryLock() {
123 return sync.OnceFunc(unlockRuntime)
124 }
125 unlockRuntime()
126 a.singleSurfaceMu.Lock()
127 a.singleSurfaceMu.Unlock() //nolint:staticcheck // SA2001: wait for the competing surface owner without retaining the runtime barrier.
128 }
129 }
130
130 lines GO