返回 DeepSeek-Reasonix
session_open_surface.go
根目录 / desktop / session_open_surface.go
1 package main
2
3 import (
4 "fmt"
5 "os"
6
7 "reasonix/desktop/internal/workspacestate"
8 "reasonix/internal/control"
9 "reasonix/internal/session"
10 )
11
12 // surfaceForCanonicalSession returns the tab OpenSession installs ref into.
13 // The active tab wins. Without one (fresh start, remote-only layout, or the
14 // last visible session was archived), a visible tab that already runs ref is
15 // activated, then a dormant local tab is reused, and otherwise an empty
16 // surface is created for ref's workspace and bound by the caller. Opening a
17 // session must never depend on a replacement blank session having been
18 // created for it.
19 func (a *App) surfaceForCanonicalSession(ref session.SessionRef, workspace workspacestate.Workspace) (*WorkspaceTab, control.SessionAPI, bool, error) {
20 if tab, ctrl := a.tabAndCtrlByID(""); tab != nil {
21 return tab, ctrl, false, nil
22 }
23 a.mu.Lock()
24 if owner := a.liveRuntimeTabMatchingLocked(nil, sessionRoute(ref.SessionID)); owner != nil && a.tabs[owner.ID] == owner && !owner.removed {
25 a.activeTabID = owner.ID
26 a.saveTabsLocked()
27 ctrl := owner.Ctrl
28 a.mu.Unlock()
29 return owner, ctrl, false, nil
30 }
31 a.mu.Unlock()
32
33 if tab, ctrl := a.firstResolvableLocalTab(); tab != nil {
34 // A remote-only layout keeps one dormant local tab for local work;
35 // opening a session there beats creating a second surface.
36 a.mu.Lock()
37 if a.tabs[tab.ID] == tab && !tab.removed {
38 a.activeTabID = tab.ID
39 a.saveTabsLocked()
40 a.mu.Unlock()
41 return tab, ctrl, false, nil
42 }
43 a.mu.Unlock()
44 }
45
46 scope := canonicalWorkspaceScope(workspace)
47 workspaceRoot := ""
48 if scope == "project" {
49 workspaceRoot = normalizeProjectRoot(workspace.Root)
50 if workspaceRoot == "" {
51 return nil, nil, false, fmt.Errorf("session workspace root is unavailable")
52 }
53 }
54 actualRoot := desktopWorkspaceRoot(scope, workspaceRoot)
55 if scope != "project" {
56 if err := os.MkdirAll(actualRoot, 0o755); err != nil {
57 return nil, nil, false, fmt.Errorf("create global workspace: %w", err)
58 }
59 }
60 releaseAdmission, err := a.beginProjectRuntimeAdmission(scope, actualRoot)
61 if err != nil {
62 return nil, nil, false, err
63 }
64 defer releaseAdmission()
65 if scope == "project" {
66 saveWorkspace(workspaceRoot)
67 a.registerProjectRoot(workspaceRoot)
68 }
69 model, toolApprovalMode := desktopNewSessionDefaults(scope, actualRoot)
70 tab := &WorkspaceTab{
71 Scope: scope,
72 WorkspaceRoot: actualRoot,
73 model: model,
74 mode: tabModeFromAxes(false, toolApprovalMode == control.ToolApprovalDangerFullAccess),
75 toolApprovalMode: toolApprovalMode,
76 disabledMCP: map[string]ServerView{},
77 }
78 a.mu.Lock()
79 if existing := a.activeTabLocked(); existing != nil {
80 // Another navigation published a surface meanwhile; install into it.
81 ctrl := existing.Ctrl
82 a.mu.Unlock()
83 return existing, ctrl, false, nil
84 }
85 tab.ID = a.newUniqueTabIDLocked()
86 tab.sink = &tabEventSink{tabID: tab.ID, app: a}
87 a.tabs[tab.ID] = tab
88 a.tabOrder = append(a.tabOrder, tab.ID)
89 a.activeTabID = tab.ID
90 a.saveTabsLocked()
91 a.mu.Unlock()
92 return tab, nil, true, nil
93 }
94
95 // discardUnboundSurface removes a surface created for OpenSession whose
96 // binding failed, so a failed open leaves no empty tab behind.
97 func (a *App) discardUnboundSurface(tab *WorkspaceTab) {
98 if tab == nil {
99 return
100 }
101 a.mu.Lock()
102 defer a.mu.Unlock()
103 if a.tabs[tab.ID] != tab || tab.Ctrl != nil || tab.SessionID != "" {
104 return
105 }
106 a.markTabRemovedLocked(tab)
107 delete(a.tabs, tab.ID)
108 a.removeTabOrderLocked(tab.ID)
109 if a.activeTabID == tab.ID {
110 a.activeTabID = ""
111 if len(a.tabOrder) > 0 {
112 a.activeTabID = a.tabOrder[0]
113 }
114 }
115 a.saveTabsLocked()
116 }
117
117 lines GO