返回 DeepSeek-Reasonix
remote_tab_persistence.go
根目录 / desktop / remote_tab_persistence.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "strings"
8 )
9
10 type desktopRemoteTabEntry struct {
11 ID string `json:"id"`
12 HostID string `json:"hostId"`
13 Workspace string `json:"workspace"`
14 TopicTitle string `json:"topicTitle,omitempty"`
15 Model string `json:"model,omitempty"`
16 SessionName string `json:"sessionName,omitempty"`
17 SessionPath string `json:"sessionPath,omitempty"`
18 SessionID string `json:"sessionId,omitempty"`
19 SessionReset bool `json:"sessionReset,omitempty"`
20 extra map[string]json.RawMessage
21 }
22
23 func singleSurfaceTabsFile(f desktopTabsFile) desktopTabsFile {
24 if len(f.Tabs)+len(f.RemoteTabs) <= 1 {
25 return f
26 }
27 active := strings.TrimSpace(f.ActiveTab)
28 for _, entry := range f.RemoteTabs {
29 if entry.ID != active {
30 continue
31 }
32 // The remote surface stays active, but one local entry is kept so local
33 // commands still have a workspace tab to target. It restores dormant
34 // (see restoreDormantWorkspaceTab), so it adds no hidden startup work.
35 if len(f.Tabs) == 0 {
36 return desktopTabsFile{
37 ActiveTab: entry.ID,
38 RemoteTabs: []desktopRemoteTabEntry{entry},
39 RemoteTabOrder: []string{entry.ID},
40 TabOrder: []string{entry.ID},
41 extra: cloneDesktopJSONFields(f.extra),
42 }
43 }
44 local := f.Tabs[0]
45 return desktopTabsFile{
46 Tabs: []desktopTabEntry{local},
47 ActiveTab: entry.ID,
48 RemoteTabs: []desktopRemoteTabEntry{entry},
49 RemoteTabOrder: []string{entry.ID},
50 // The remote surface keeps the head of the strip order so any
51 // first-tab fallback still resolves to the surface the user left on.
52 TabOrder: []string{entry.ID, local.ID},
53 extra: cloneDesktopJSONFields(f.extra),
54 }
55 }
56 for _, entry := range f.Tabs {
57 if entry.ID == active {
58 return desktopTabsFile{Tabs: []desktopTabEntry{entry}, ActiveTab: entry.ID, TabOrder: []string{entry.ID}, extra: cloneDesktopJSONFields(f.extra)}
59 }
60 }
61 if len(f.Tabs) > 0 {
62 return desktopTabsFile{Tabs: []desktopTabEntry{f.Tabs[0]}, ActiveTab: f.Tabs[0].ID, TabOrder: []string{f.Tabs[0].ID}, extra: cloneDesktopJSONFields(f.extra)}
63 }
64 chosen := f.RemoteTabs[0]
65 return desktopTabsFile{ActiveTab: chosen.ID, RemoteTabs: []desktopRemoteTabEntry{chosen}, RemoteTabOrder: []string{chosen.ID}, TabOrder: []string{chosen.ID}, extra: cloneDesktopJSONFields(f.extra)}
66 }
67
68 // remoteSurfaceIsActiveTab reports whether the persisted active tab is a remote
69 // entry: the remote shell then owns the visible surface, and restored local tabs
70 // stay dormant (no runtime) so they add no hidden startup work.
71 func remoteSurfaceIsActiveTab(f desktopTabsFile) bool {
72 active := strings.TrimSpace(f.ActiveTab)
73 if active == "" {
74 return false
75 }
76 for _, entry := range f.RemoteTabs {
77 if strings.TrimSpace(entry.ID) == active {
78 return true
79 }
80 }
81 return false
82 }
83
84 // saveTabsFromRemote snapshots local state before joining it with the remote
85 // registry. Callers must not hold remoteTabMu.
86 func (a *App) saveTabsFromRemote() {
87 a.mu.Lock()
88 dir, entries, activeID, version := a.saveTabsCollectLocked()
89 a.mu.Unlock()
90 a.saveTabsWrite(dir, entries, activeID, version)
91 }
92
93 // finishRestoredLocalTabs activates and builds the restored local tabs. When the
94 // persisted active tab is a remote shell they stay dormant instead: the remote
95 // surface keeps the visible surface and no hidden startup work is added. Their
96 // controller is built when they are first activated or used.
97 func (a *App) finishRestoredLocalTabs(f desktopTabsFile, toBuild []*WorkspaceTab) {
98 if remoteSurfaceIsActiveTab(f) {
99 return
100 }
101 a.mu.Lock()
102 if _, ok := a.tabs[f.ActiveTab]; ok {
103 a.activeTabID = f.ActiveTab
104 } else if ordered := a.orderedTabIDsLocked(); len(ordered) > 0 {
105 a.activeTabID = ordered[0]
106 }
107 a.saveTabsLocked()
108 a.mu.Unlock()
109 for _, tab := range toBuild {
110 a.startTabControllerBuild(tab)
111 }
112 }
113
114 // restoreDormantWorkspaceTab publishes one local workspace tab with no session
115 // and no runtime. A remote-only layout leaves the visible surface to the remote
116 // shell, but local commands still need a tab to target; this tab is activated
117 // on first use and never takes the surface from the remote tab.
118 func (a *App) restoreDormantWorkspaceTab(ctx context.Context) {
119 admissionRelease, err := a.beginProjectRuntimeAdmission("global", globalTabWorkspaceRoot())
120 if err != nil {
121 slog.Warn("desktop: dormant workspace tab admission failed", "err", err)
122 return
123 }
124 tab := a.createTabEntry("global", globalTabWorkspaceRoot(), "")
125 tab.sink = &tabEventSink{tabID: tab.ID, app: a, ctx: ctx}
126 a.publishRestoredTab(tab, admissionRelease)
127 }
128
128 lines GO