返回 DeepSeek-Reasonix
session_new.go
根目录 / desktop / session_new.go
1 package main
2
3 import (
4 "fmt"
5
6 "reasonix/internal/config"
7 "reasonix/internal/control"
8 )
9
10 // NewSession snapshots the current conversation and rotates to a fresh one.
11 func (a *App) NewSession() error {
12 return a.NewSessionForTab("")
13 }
14
15 // NewSessionForTab snapshots and rotates the requested tab regardless of which
16 // tab becomes active while the Wails call is in flight.
17 func (a *App) NewSessionForTab(tabID string) error {
18 tab, ctrl := a.tabAndCtrlByID(tabID)
19 if a.tabIsReadOnly(tab) {
20 return readOnlyChannelErr()
21 }
22 if ctrl == nil {
23 return a.workspaceNotReadyErr(tab)
24 }
25 if err := a.ensureTabControllerWorkspace(tab); err != nil {
26 return err
27 }
28 ctrl = a.controllerForTab(tab)
29 if ctrl == nil {
30 return a.workspaceNotReadyErr(tab)
31 }
32 // Serialize the session rotation with rebuilds, foreground admission, and
33 // Pin/Unpin. The default-model rebuild starts only after these locks release.
34 unlockRuntime := a.lockRuntimeMutation("new session")
35 tab.turnStartMu.Lock()
36 released := false
37 releaseAdmission := func() {
38 if released {
39 return
40 }
41 released = true
42 tab.turnStartMu.Unlock()
43 unlockRuntime()
44 }
45 defer releaseAdmission()
46 ctrl = a.controllerForTab(tab)
47 if ctrl == nil {
48 return a.workspaceNotReadyErr(tab)
49 }
50 // Tab is already blank — skip rotation, but still apply the configured
51 // default. A reused empty tab otherwise keeps the previous session's
52 // provider after a default-model change (#9080).
53 if !controllerHasActiveRuntimeWork(ctrl) && !messagesHaveConversationContent(ctrl.History()) {
54 if err := clearBlankSessionPinnedContext(tab, ctrl); err != nil {
55 return err
56 }
57 a.persistTabSessionPath(tab, ctrl.SessionPath())
58 a.mu.RLock()
59 reused := tab.SessionID
60 a.mu.RUnlock()
61 if reused != "" {
62 // The blank session is handed out as new, so it keeps no earlier
63 // choice and no snapshot read before this point can make one.
64 a.sessionPresets.forget(reused)
65 a.adoptSessionPreset(tab, ctrl, reused)
66 if invalidator, ok := ctrl.(permissionSnapshotInvalidator); ok {
67 invalidator.InvalidatePermissionSnapshots()
68 }
69 }
70 releaseAdmission()
71 if err := a.ensureReusableBlankIdentity(tab); err != nil {
72 return err
73 }
74 return a.applyNewSessionDefaultModel(tab)
75 }
76
77 if err := ctrl.NewSession(); err != nil {
78 a.syncTabSessionIdentity(tab, ctrl)
79 return err
80 }
81 a.syncTabSessionIdentity(tab, ctrl)
82 if path := ctrl.SessionPath(); path != "" {
83 if err := savePinnedContextState(path, []string{}); err != nil {
84 return fmt.Errorf("initialize empty pinned context for new session: %w", err)
85 }
86 }
87 tab.setPinnedFiles(nil)
88 // The rotated session starts with zero spend: without this reset the tab
89 // telemetry keeps the previous session's totals and the status bar 会话费用
90 // silently turns into an all-sessions running total (#5850).
91 tab.resetTelemetry(tab.currentSessionIdentity())
92 // Mirror the controller: NewSession cleared the active goal, and the tab's
93 // persisted copy must follow — otherwise the next rebuild/restart would
94 // re-seed the old goal into the fresh session via SetGoal(tab.goal).
95 a.clearTabGoal(tab)
96 if err := a.assignFreshSessionTopic(tab); err != nil {
97 return fmt.Errorf("session created; persist topic presentation: %w", err)
98 }
99 a.persistTabSessionPath(tab, ctrl.SessionPath())
100 a.invalidatePromptHistoryCache()
101 a.emitProjectTreeChangedForSessionDirs(ctrl.SessionDir())
102 releaseAdmission()
103 return a.applyNewSessionDefaultModel(tab)
104 }
105
106 func (a *App) syncTabSessionIdentity(tab *WorkspaceTab, ctrl control.SessionAPI) {
107 if a == nil || tab == nil || ctrl == nil {
108 return
109 }
110 identity, ok := ctrl.(control.IdentityLifecycle)
111 if !ok || !identity.UsesExclusiveSession() {
112 return
113 }
114 ref, bound := identity.SessionRef()
115 if !bound {
116 return
117 }
118 rebound := false
119 a.mu.Lock()
120 if current := a.tabs[tab.ID]; current == tab {
121 if tab.SessionID == "" && tab.SessionPath != "" && identity.SessionService() != a.desktopSessionService("") {
122 // A historical directory has an identity only within its original
123 // store. Keep the path locator until a new canonical session is bound.
124 a.mu.Unlock()
125 return
126 }
127 if tab.SessionID != ref.SessionID {
128 tab.SessionGeneration++
129 if tab.sink != nil {
130 tab.sink.setSessionGeneration(tab.SessionGeneration)
131 }
132 rebound = true
133 }
134 tab.SessionID = ref.SessionID
135 tab.SessionHeadID = ""
136 tab.SessionPath = ""
137 a.bindSessionRuntimeKeyLocked(tab, tab.currentSessionIdentity())
138 }
139 a.mu.Unlock()
140 if rebound {
141 a.adoptSessionPreset(tab, ctrl, ref.SessionID)
142 }
143 a.saveTabsFromRemote()
144 }
145
146 type permissionSnapshotInvalidator interface {
147 InvalidatePermissionSnapshots()
148 }
149
150 // adoptSessionPreset gives a tab that now holds sessionID that session's own
151 // recorded preset, or the new-session default; the preset the surface held for
152 // its previous session never carries over.
153 func (a *App) adoptSessionPreset(tab *WorkspaceTab, ctrl control.SessionAPI, sessionID string) {
154 preset := a.sessionPresets.restore(sessionID, newSessionPreset(config.LoadForEdit(config.UserConfigPath())))
155 a.mu.Lock()
156 if a.tabs[tab.ID] != tab || tab.SessionID != sessionID {
157 a.mu.Unlock()
158 return
159 }
160 tab.toolApprovalMode = preset
161 tab.mode = tabModeFromAxes(tabModeHasPlan(tab.mode), preset == control.ToolApprovalDangerFullAccess)
162 a.mu.Unlock()
163 applyTabToolApprovalModeToController(ctrl, preset)
164 }
165
166 func clearBlankSessionPinnedContext(tab *WorkspaceTab, ctrl control.SessionAPI) error {
167 oldFiles := tab.GetPinnedFiles()
168 path := ctrl.SessionPath()
169 if path != "" {
170 if err := savePinnedContextState(path, []string{}); err != nil {
171 return err
172 }
173 }
174 if len(oldFiles) > 0 {
175 tab.setPinnedFiles(nil)
176 }
177 return nil
178 }
179
180 func installClearedTabRuntime(tab *WorkspaceTab, ctrl control.SessionAPI, sink *tabEventSink, path string) {
181 tab.Ctrl = ctrl
182 tab.sink = sink
183 tab.SessionPath = path
184 tab.Label = ctrl.Label()
185 tab.Ready = true
186 tab.setPinnedFiles(nil)
187 }
188
188 lines GO