返回 DeepSeek-Reasonix
history_search_collect.go
根目录 / desktop / history_search_collect.go
1 package main
2
3 import (
4 "context"
5 "path/filepath"
6 "strings"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/historycatalog"
10 "reasonix/internal/provider"
11 "reasonix/internal/retrieval"
12 )
13
14 func historySearchRootFilter(a *App, req HistorySearchRequest) []string {
15 rootFilter := []string{}
16 for _, root := range historyCatalogRoots(a.sessionCatalogTargets()) {
17 if req.Scope == "project" && (root.Scope != "project" || !sameProjectRoot(root.WorkspaceRoot, req.WorkspaceRoot)) {
18 continue
19 }
20 if req.Scope == "global" && root.Scope == "project" {
21 continue
22 }
23 rootFilter = append(rootFilter, root.Path)
24 }
25 return rootFilter
26 }
27
28 func historyStatusMatches(status string, open, current bool) bool {
29 switch strings.TrimSpace(status) {
30 case "open":
31 // Match HistoryPanel: open-but-not-current.
32 return open && !current
33 case "current":
34 return current
35 default:
36 return true
37 }
38 }
39
40 func (a *App) historyHitFromCandidate(
41 req HistorySearchRequest,
42 candidate historycatalog.Candidate,
43 active string,
44 overlays map[string]catalogRuntimeOverlay,
45 queryTerms []string,
46 loaded map[string][]provider.Message,
47 recoveryChecked map[string]bool,
48 recoveryCovered map[string]bool,
49 catalog *historycatalog.Catalog,
50 ) (HistorySearchHit, bool) {
51 if !recoveryChecked[candidate.SessionPath] {
52 recoveryChecked[candidate.SessionPath] = true
53 if sessions := a.sessionCatalog.Load(); sessions != nil {
54 if record, ok, err := sessions.GetSession(a.bootContext(), candidate.SessionPath); err == nil && ok {
55 recoveryCovered[candidate.SessionPath] = record.RecoveryCopy
56 }
57 }
58 }
59 if recoveryCovered[candidate.SessionPath] {
60 return HistorySearchHit{}, false
61 }
62 overlay := overlays[sessionRuntimeKey(candidate.SessionPath)]
63 current := candidate.SessionPath == active
64 if !historyStatusMatches(req.Status, overlay.open, current) {
65 return HistorySearchHit{}, false
66 }
67 if !historyTimeMatches(candidate.LastActivityAt, req.TimeFilter) {
68 return HistorySearchHit{}, false
69 }
70 messages, ok := loaded[candidate.SessionPath]
71 if !ok {
72 if identity, known, identityErr := agent.SessionContentIdentity(candidate.SessionPath); identityErr == nil && known &&
73 candidate.ContentDigest != "" && identity.DigestHex != candidate.ContentDigest {
74 catalog.EnqueueExisting(context.Background(), candidate.SessionPath)
75 return HistorySearchHit{}, false
76 }
77 session, loadErr := agent.LoadSession(candidate.SessionPath)
78 if loadErr != nil {
79 return HistorySearchHit{}, false
80 }
81 messages = session.Snapshot()
82 loaded[candidate.SessionPath] = messages
83 }
84 text, ok := desktopHistoryText(messages, candidate)
85 if !ok {
86 catalog.EnqueueExisting(context.Background(), candidate.SessionPath)
87 return HistorySearchHit{}, false
88 }
89 return HistorySearchHit{
90 SessionPath: candidate.SessionPath,
91 SessionID: strings.TrimSuffix(filepath.Base(candidate.SessionPath), filepath.Ext(candidate.SessionPath)),
92 Source: candidate.Source, MessageIndex: candidate.MessageIndex, Role: candidate.Role,
93 Kind: candidate.Kind, ToolName: candidate.ToolName,
94 Snippet: retrieval.MakeSnippet(text, req.Query, queryTerms, 240), Score: candidate.Score,
95 SessionTitle: candidate.SessionTitle, TopicTitle: candidate.TopicTitle, WorkspaceRoot: candidate.WorkspaceRoot,
96 LastActivityAt: candidate.LastActivityAt, Open: overlay.open, Running: overlay.running, Current: current,
97 }, true
98 }
99
100 func (a *App) collectHistorySearchItems(
101 req HistorySearchRequest,
102 catalog *historycatalog.Catalog,
103 kinds, rootFilter []string,
104 after *historycatalog.SearchCursor,
105 limit int,
106 revision uint64,
107 ) ([]HistorySearchHit, string, string) {
108 const batchLimit = 200
109 need := limit + 1
110 _, overlays := a.catalogRuntimeOverlays()
111 active := a.activeSessionPath(a.activeSessionDir())
112 queryTerms, _ := retrieval.QueryTerms(req.Query)
113 loaded := map[string][]provider.Message{}
114 recoveryChecked := map[string]bool{}
115 recoveryCovered := map[string]bool{}
116 items := []HistorySearchHit{}
117 var cursorCandidate historycatalog.Candidate
118 for len(items) < need {
119 result, searchErr := catalog.Search(a.bootContext(), historycatalog.SearchRequest{
120 Query: req.Query, Scope: req.Scope, WorkspaceRoot: req.WorkspaceRoot,
121 Kinds: kinds, ToolName: req.ToolName, Limit: batchLimit, Roots: rootFilter, After: after,
122 })
123 if searchErr != nil {
124 return items, "", searchErr.Error()
125 }
126 if len(result.Items) == 0 {
127 break
128 }
129 for _, candidate := range result.Items {
130 hit, ok := a.historyHitFromCandidate(req, candidate, active, overlays, queryTerms, loaded, recoveryChecked, recoveryCovered, catalog)
131 if !ok {
132 continue
133 }
134 items = append(items, hit)
135 if len(items) == limit {
136 cursorCandidate = candidate
137 }
138 if len(items) == need {
139 break
140 }
141 }
142 if len(items) == need {
143 break
144 }
145 last := result.Items[len(result.Items)-1]
146 after = &historycatalog.SearchCursor{Rank: last.Rank, SessionPath: last.SessionPath, MessageIndex: last.MessageIndex,
147 PartIndex: last.PartIndex, RowID: last.RowID}
148 if len(result.Items) < batchLimit {
149 break
150 }
151 }
152 nextCursor := ""
153 if len(items) > limit {
154 items = items[:limit]
155 nextCursor = encodeHistoryCursor(historySearchCursor{Revision: revision, Rank: cursorCandidate.Rank, Score: cursorCandidate.Score,
156 Path: cursorCandidate.SessionPath, Message: cursorCandidate.MessageIndex, Part: cursorCandidate.PartIndex, RowID: cursorCandidate.RowID})
157 }
158 return items, nextCursor, ""
159 }
160
160 lines GO