返回 DeepSeek-Reasonix
session_catalog_availability.go
根目录 / desktop / session_catalog_availability.go
1 package main
2
3 import (
4 "context"
5 "os"
6 "sort"
7
8 "reasonix/internal/sessioncatalog"
9 )
10
11 type catalogWorkspaceAvailability struct {
12 usable bool
13 complete bool
14 ready int
15 pending int
16 failed int
17 }
18
19 func (availability catalogWorkspaceAvailability) decorate(page ProjectTopicPage, revision uint64) ProjectTopicPage {
20 page.Revision = max(page.Revision, revision)
21 page.Complete = availability.complete
22 page.ReadyDirectories = availability.ready
23 page.PendingDirectories = availability.pending
24 page.FailedDirectories = availability.failed
25 return page
26 }
27
28 func (a *App) catalogWorkspaceAvailability(catalog *sessioncatalog.Catalog, scope, workspaceRoot string, views ...context.Context) catalogWorkspaceAvailability {
29 availability := catalogWorkspaceAvailability{}
30 if a == nil || catalog == nil {
31 return availability
32 }
33 ctx, cancel := a.catalogReadContext()
34 defer cancel()
35 if len(views) > 0 && views[0] != nil {
36 ctx = views[0]
37 }
38 scope, workspaceRoot = normalizeDesktopTopicScope(scope, workspaceRoot)
39 for _, target := range a.sessionCatalogTargets() {
40 if target.Scope != scope || scope == "project" && !sameProjectRoot(target.WorkspaceRoot, workspaceRoot) {
41 continue
42 }
43 if !catalog.MetadataOnly() {
44 if _, err := os.Stat(target.Path); os.IsNotExist(err) {
45 continue
46 }
47 }
48 // Metadata discovery owns absence: an unused optional root completes
49 // empty, while a disappeared root with retained history is unavailable.
50 // Re-statting here used to silently omit that failure and claim complete.
51 switch catalog.DirectoryStatus(ctx, target.Path).State {
52 case "ready":
53 availability.ready++
54 case "degraded":
55 availability.failed++
56 default:
57 availability.pending++
58 }
59 }
60 hasRecords := catalog.HasWorkspaceRecords(ctx, scope, workspaceRoot)
61 availability.usable = hasRecords || availability.ready > 0
62 availability.complete = availability.pending == 0 && availability.failed == 0 && availability.ready > 0
63 if availability.ready+availability.pending+availability.failed == 0 && hasRecords {
64 availability.complete = true
65 }
66 return availability
67 }
68
69 func (a *App) mergeMetadataTopics(req ProjectTopicPageRequest, page ProjectTopicPage) (ProjectTopicPage, error) {
70 metadata, err := a.metadataTopicPage(req)
71 if err != nil {
72 return page, err
73 }
74 manualOrder := manualTopicOrderFor(req.Scope, req.WorkspaceRoot)
75 if req.readAllSources {
76 manualOrder = false
77 }
78 seen := make(map[string]struct{}, len(page.Items)+len(metadata.Items))
79 for _, item := range page.Items {
80 seen[item.TopicID] = struct{}{}
81 }
82 for _, item := range metadata.Items {
83 if _, exists := seen[item.TopicID]; exists {
84 continue
85 }
86 seen[item.TopicID] = struct{}{}
87 page.Items = append(page.Items, item)
88 }
89 sort.SliceStable(page.Items, func(i, j int) bool {
90 return projectTopicLess(page.Items[i], page.Items[j], req.SortMode, manualOrder)
91 })
92 limit := req.Limit
93 if limit <= 0 {
94 limit = sessioncatalog.DefaultLimit
95 }
96 if limit > sessioncatalog.MaxLimit {
97 limit = sessioncatalog.MaxLimit
98 }
99 hasMore := page.NextCursor != "" || metadata.NextCursor != "" || len(page.Items) > limit
100 if len(page.Items) > limit {
101 page.Items = page.Items[:limit]
102 }
103 page.NextCursor = ""
104 if hasMore && len(page.Items) > 0 {
105 page.NextCursor = encodeProjectNodeCursor(page.Items[len(page.Items)-1], req.SortMode, manualOrder, req.groupCursorBind)
106 }
107 return page, nil
108 }
109
109 lines GO