返回 DeepSeek-Reasonix
historical_catalog.go
根目录 / desktop / historical_catalog.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "sort"
8 "time"
9
10 "reasonix/desktop/internal/workspacestate"
11 "reasonix/internal/session"
12 )
13
14 type historicalCatalogEntry struct {
15 scope string
16 node ProjectNode
17 }
18
19 // Discovery publishes metadata only; ordinary pagination never visits source
20 // directories or replays a historical log. Refreshes share one bounded worker.
21 func (a *App) requestHistoricalCatalog() {
22 c := &a.historicalImports
23 c.mu.Lock()
24 c.initialize(a.bootContext())
25 if !c.catalogEnabled || c.stopped || a.shuttingDown.Load() || c.discoveryPending || time.Since(c.catalogAt) < 5*time.Second {
26 c.mu.Unlock()
27 return
28 }
29 c.discoveryPending = true
30 c.workers.Add(1)
31 c.mu.Unlock()
32 go func() {
33 defer c.workers.Done()
34 _, _ = a.listHistoricalSessions(c.ctx)
35 c.mu.Lock()
36 c.discoveryPending = false
37 stopped := c.stopped
38 c.mu.Unlock()
39 if !stopped {
40 a.emitProjectTreeChanged()
41 }
42 }()
43 }
44
45 func readHistoricalCanonicalCatalog(ctx context.Context, sources map[string]historicalSource) []historicalCatalogEntry {
46 rows := []historicalCatalogEntry{}
47 for key, source := range sources {
48 if ctx.Err() != nil {
49 break
50 }
51 if source.format != "canonical" || source.version != "" {
52 continue
53 }
54 kind := "global_topic"
55 if source.scope == "project" {
56 kind = "topic"
57 }
58 node := ProjectNode{Key: "source_" + key, Kind: kind, Root: source.root, Label: filepath.Base(source.path),
59 TopicID: "historical-" + key, Historical: true, SessionPath: source.path, SortOrder: -1,
60 TurnsState: "unknown", Health: "metadata_pending", Children: []ProjectNode{},
61 Source: &SessionSourceRef{HostID: localDesktopHostID, SourceKey: key, Path: source.path}}
62 if info, err := session.NewFilesystemPersistence(filepath.Dir(source.path)).Stat(ctx, filepath.Base(source.path)); err == nil {
63 if info.Title != "" {
64 node.Label = info.Title
65 }
66 node.Preview, node.Turns = info.Preview, info.Turns
67 node.CreatedAt, node.LastActivityAt = info.CreatedAt.UnixMilli(), info.UpdatedAt.UnixMilli()
68 if info.MetadataStatus == session.MetadataReady {
69 node.TurnsState, node.Health = "valid", "ok"
70 }
71 } else if stat, statErr := os.Stat(source.path); statErr == nil {
72 node.CreatedAt, node.LastActivityAt = stat.ModTime().UnixMilli(), stat.ModTime().UnixMilli()
73 node.Health = "degraded"
74 }
75 rows = append(rows, historicalCatalogEntry{scope: source.scope, node: node})
76 }
77 return rows
78 }
79
80 func (a *App) historicalCanonicalTopics(scope, root string, state workspacestate.State) []ProjectNode {
81 a.requestHistoricalCatalog()
82 c := &a.historicalImports
83 c.mu.Lock()
84 defer c.mu.Unlock()
85 rows := []ProjectNode{}
86 for _, entry := range c.catalog {
87 if entry.scope != scope || scope == "project" && !sameDesktopPath(entry.node.Root, root) {
88 continue
89 }
90 node := entry.node
91 if _, adopted := historicalMappingForSource(state, node.Source.SourceKey); adopted {
92 continue
93 }
94 node.PreparationStatus = "available"
95 if view, ok := c.views[node.Source.SourceKey]; ok {
96 node.PreparationStatus = view.Status
97 }
98 rows = append(rows, node)
99 }
100 return rows
101 }
102
103 func applyHistoricalPresentations(nodes []ProjectNode, saved historicalImportQueueSidecar) {
104 for i := range nodes {
105 if nodes[i].Source == nil {
106 continue
107 }
108 presentation := saved.Presentations[nodes[i].Source.SourceKey]
109 if presentation.Title != "" {
110 nodes[i].Label = presentation.Title
111 }
112 if presentation.Pinned != nil {
113 nodes[i].Pinned = *presentation.Pinned
114 }
115 }
116 }
117
118 // A shell-only read must not create workspaces or migrate organization state.
119 // Sources without canonical members still need their persisted pin overlays.
120 func (a *App) historicalPinnedShells(req ProjectTopicPageRequest, state workspacestate.State) ([]ProjectNode, error) {
121 adopted := map[string]bool{}
122 for _, mapping := range state.SourceMappings {
123 adopted["source\x00local\x00"+mapping.SourceKey] = true
124 if sourceMappingHasPathAlias(mapping) {
125 adopted[sessionRuntimeKey(mapping.Path)] = true
126 }
127 }
128 page, err := a.unadoptedLegacyTopics(req, adopted, nil)
129 if err != nil {
130 return nil, err
131 }
132 nodes := append(page.Items, a.historicalCanonicalTopics(req.Scope, req.WorkspaceRoot, state)...)
133 if saved, err := readHistoricalSidecar(); err == nil {
134 applyHistoricalPresentations(nodes, saved)
135 }
136 pins := []ProjectNode{}
137 for _, node := range nodes {
138 if node.Pinned {
139 pins = append(pins, node)
140 }
141 }
142 sort.SliceStable(pins, func(i, j int) bool { return projectTopicLess(pins[i], pins[j], req.SortMode, false) })
143 return pins, nil
144 }
145
145 lines GO