返回 DeepSeek-Reasonix
session_catalog_legacy_rows.go
根目录 / desktop / session_catalog_legacy_rows.go
1 package main
2
3 import (
4 "reasonix/internal/agent"
5 "reasonix/internal/sessioncatalog"
6 "sort"
7 "strings"
8 )
9
10 // projectNodesFromCatalogTopic expands every ordinary, independently
11 // addressable legacy session into its own sidebar row. TopicID remains shared
12 // history metadata; SessionPath is the operation and navigation target.
13 // Recovery-only topics intentionally keep their single summary row because
14 // their physical replicas are managed by the saved-versions surface.
15 func (a *App) projectNodesFromCatalogTopic(topic sessioncatalog.TopicRecord, topicOverlays, sessionOverlays map[string]catalogRuntimeOverlay, preferred map[string]struct{}) []ProjectNode {
16 base, visibleTopic := a.projectNodeFromCatalogTopic(topic, topicOverlays, sessionOverlays, preferred)
17 if !visibleTopic {
18 return []ProjectNode{}
19 }
20 if topic.RecoveryState == "recovery_only" {
21 return []ProjectNode{base}
22 }
23 localPreferred := preferred
24 if localPreferred == nil {
25 localPreferred = sessioncatalog.PreferredOrdinarySessionPaths(topic.Sessions)
26 }
27 kind := "topic"
28 if topic.Scope == "global" {
29 kind = "global_topic"
30 }
31 nodes := make([]ProjectNode, 0, len(topic.Sessions))
32 projects := loadProjectsFile()
33 coveredRuntime := coveredLegacyRuntime(topic.Sessions, sessionOverlays, localPreferred)
34 for _, record := range topic.Sessions {
35 overlay := sessionOverlays[sessionRuntimeKey(record.Path)]
36 // Covered recovery copies remain represented by their logical parent in
37 // the ordinary tree, even while an older restored tab still has the copy
38 // open. The dedicated recovery surface owns the physical copy.
39 if hiddenLegacyRecovery(record, localPreferred) {
40 continue
41 }
42 if !sessioncatalog.OrdinaryTreeSession(record, overlay.open, overlay.running, localPreferred) {
43 continue
44 }
45 if inherited := coveredRuntime[agent.BranchID(record.Path)]; inherited.open || inherited.running {
46 overlay.open = overlay.open || inherited.open
47 overlay.running = overlay.running || inherited.running
48 if overlay.status == "" {
49 overlay.status = inherited.status
50 }
51 }
52 label := strings.TrimSpace(record.CustomTitle)
53 if label == "" {
54 label = a.localizedTopicTitle(topic.Title, topic.TitleSource)
55 }
56 turnsState := string(record.TurnsState)
57 if turnsState == "" {
58 turnsState = base.TurnsState
59 }
60 health := string(record.Health)
61 if health == "" {
62 health = base.Health
63 }
64 node := ProjectNode{
65 Key: projectSessionNodeKey(topic.Scope, record.Path), Kind: kind, Label: label,
66 Root: topic.WorkspaceRoot, TopicID: topic.TopicID, SessionPath: record.Path,
67 Preview: strings.TrimSpace(record.Preview), Turns: record.Turns,
68 TurnsState: turnsState, Health: health,
69 CreatedAt: record.CreatedAt, LastActivityAt: record.LastActivityAt,
70 Pinned: topic.Pinned, SortOrder: topic.SortOrder,
71 Recovered: record.Recovered, RecoveryReason: record.RecoveryReason,
72 RecoveryDigest: record.RecoveryDigest, RecoveryParentID: record.ParentID,
73 Open: overlay.open, Running: overlay.running, Status: overlay.status,
74 Children: []ProjectNode{},
75 }
76 if rank := sessionOrderRank(projects, topic.Scope, topic.WorkspaceRoot, projectNodeSessionKey(node)); rank >= 0 {
77 node.SortOrder = rank
78 } else if (topic.Scope == "global" && projects.GlobalManualSessionOrder) || (topic.Scope == "project" && projectManualSessionOrder(projects, topic.WorkspaceRoot)) {
79 node.SortOrder = -1
80 }
81 if node.Preview == "" && len(topic.Sessions) == 1 {
82 node.Preview = base.Preview
83 }
84 nodes = append(nodes, node)
85 }
86 if len(nodes) == 0 {
87 // Pinned and runtime-only topic shells remain reachable while their
88 // catalog session is incomplete.
89 return []ProjectNode{base}
90 }
91 manual := manualSessionOrderFor(topic.Scope, topic.WorkspaceRoot)
92 sort.SliceStable(nodes, func(i, j int) bool {
93 if manual && nodes[i].SortOrder != nodes[j].SortOrder {
94 left, right := nodes[i].SortOrder, nodes[j].SortOrder
95 if left < 0 {
96 left = int(^uint(0) >> 1)
97 }
98 if right < 0 {
99 right = int(^uint(0) >> 1)
100 }
101 return left < right
102 }
103 if nodes[i].LastActivityAt != nodes[j].LastActivityAt {
104 return nodes[i].LastActivityAt > nodes[j].LastActivityAt
105 }
106 if nodes[i].CreatedAt != nodes[j].CreatedAt {
107 return nodes[i].CreatedAt > nodes[j].CreatedAt
108 }
109 return nodes[i].Key < nodes[j].Key
110 })
111 return nodes
112 }
113
114 func projectManualSessionOrder(projects desktopProjectFile, workspaceRoot string) bool {
115 index := projectIndexByRoot(projects.Projects, workspaceRoot)
116 return index >= 0 && projects.Projects[index].ManualSessionOrder
117 }
118
119 func hiddenLegacyRecovery(record sessioncatalog.SessionRecord, localPreferred map[string]struct{}) bool {
120 if record.RecoveryCopy || record.RecoveryRole == sessioncatalog.RecoveryRoleCoveredCopy {
121 return true
122 }
123 if !record.Recovered || localPreferred == nil {
124 return false
125 }
126 _, visible := localPreferred[strings.TrimSpace(record.Path)]
127 return !visible
128 }
129
130 func coveredLegacyRuntime(records []sessioncatalog.SessionRecord, sessionOverlays map[string]catalogRuntimeOverlay, localPreferred map[string]struct{}) map[string]catalogRuntimeOverlay {
131 coveredRuntime := map[string]catalogRuntimeOverlay{}
132 for _, record := range records {
133 if !hiddenLegacyRecovery(record, localPreferred) {
134 continue
135 }
136 overlay := sessionOverlays[sessionRuntimeKey(record.Path)]
137 if !overlay.open && !overlay.running {
138 continue
139 }
140 parentID := strings.TrimSpace(record.ParentID)
141 if parentID == "" {
142 parentID = strings.TrimSpace(record.RecoveryGroupID)
143 }
144 if parentID == "" {
145 continue
146 }
147 current := coveredRuntime[parentID]
148 current.open = current.open || overlay.open
149 current.running = current.running || overlay.running
150 if current.status == "" {
151 current.status = overlay.status
152 }
153 coveredRuntime[parentID] = current
154 }
155 return coveredRuntime
156 }
157
157 lines GO