返回 DeepSeek-Reasonix
history_read_snapshots.go
根目录 / desktop / history_read_snapshots.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "sort"
8 "strings"
9
10 "reasonix/internal/sessioncatalog"
11 )
12
13 func snapshotReadError(err error) (*ReadError, bool) {
14 if err == nil {
15 return nil, false
16 }
17 var op *SessionOperationError
18 if errors.As(err, &op) && op.Code == "stale_cursor" {
19 return &ReadError{Code: "stale_cursor", Reason: op.ReadReason, Message: err.Error()}, true
20 }
21 return &ReadError{Code: "read_failed", Reason: "read_failed", Message: err.Error()}, false
22 }
23
24 func (a *App) ListHistorySessions(req HistorySessionPageRequest) HistorySessionPage {
25 out := HistorySessionPage{Items: []SessionMeta{}}
26 key := req
27 key.Cursor, key.Limit = "", 0
28 active := a.activeSessionPath(a.activeSessionDir())
29 binding := snapshotBinding("history-sessions", []any{key, active})
30 store := &a.desktopSessions.readSnapshots
31 var first *readSnapshot
32 var err error
33 if req.Cursor == "" {
34 first, err = store.build(a.bootContext(), binding, func(ctx context.Context, snap *readSnapshot) error {
35 meta := HistorySessionPage{Partial: true}
36 catalog := a.sessionCatalog.Load()
37 if catalog == nil {
38 snap.metadata, _ = json.Marshal(meta)
39 return nil
40 }
41 _, overlays := a.catalogRuntimeOverlays()
42 fence, err := a.newReadSourceFence(store, snap)
43 if err != nil {
44 return err
45 }
46 fence.metadataOnly = true
47 err = catalog.WithReadView(ctx, func(view context.Context) error {
48 cursor := ""
49 for {
50 page, err := catalog.ListSessions(view, sessioncatalog.SessionPageRequest{Scope: req.Scope, WorkspaceRoot: req.WorkspaceRoot, Query: req.Query, TimeFilter: req.TimeFilter, Cursor: cursor, Limit: 200})
51 if err != nil {
52 return err
53 }
54 if page.StaleCursor {
55 return snapshotStale("lifecycle_changed")
56 }
57 meta.Revision = page.Revision
58 for _, row := range page.Items {
59 overlay := overlays[sessionRuntimeKey(row.Path)]
60 if !historyStatusMatches(req.Status, overlay.open, row.Path == active) {
61 continue
62 }
63 if err := fence.add(view, row.Path); err != nil {
64 return err
65 }
66 if err := store.append(ctx, snap, sessionMetaFromCatalog(row, row.Path == active, overlay.open)); err != nil {
67 return err
68 }
69 }
70 if page.NextCursor == "" {
71 return nil
72 }
73 if page.NextCursor == cursor {
74 return errors.New("history catalog cursor did not advance")
75 }
76 cursor = page.NextCursor
77 }
78 })
79 if err != nil {
80 return err
81 }
82 status := catalog.Status()
83 snap.validate = fence.freeze()
84 meta.Partial = status.State != sessioncatalog.StateReady || status.Indexed < status.Total
85 snap.metadata, err = json.Marshal(meta)
86 return err
87 })
88 }
89 if err == nil {
90 var meta json.RawMessage
91 out.NextCursor, out.SnapshotID, out.SnapshotExpiresAt, meta, err = store.page(a.bootContext(), binding, req.Cursor, first, req.Limit, func(b []byte) error {
92 var row SessionMeta
93 if err := json.Unmarshal(b, &row); err != nil {
94 return err
95 }
96 out.Items = append(out.Items, row)
97 return nil
98 })
99 if err == nil {
100 var frozen HistorySessionPage
101 err = json.Unmarshal(meta, &frozen)
102 out.Revision, out.Partial = frozen.Revision, frozen.Partial
103 }
104 }
105 if err != nil {
106 out = HistorySessionPage{Items: []SessionMeta{}}
107 out.ReadError, out.StaleCursor = snapshotReadError(err)
108 }
109 return out
110 }
111
112 func (a *App) SearchHistoryContent(req HistorySearchRequest) HistorySearchPage {
113 return a.searchHistorySnapshot(req, "")
114 }
115
116 func (a *App) searchHistorySnapshot(req HistorySearchRequest, targetPath string) HistorySearchPage {
117 out := HistorySearchPage{Items: []HistorySearchHit{}}
118 req.Query = strings.TrimSpace(req.Query)
119 req.Kinds = append([]string(nil), req.Kinds...)
120 if len(req.Kinds) == 0 {
121 // The panel searches what the user saw. Every tool result is indexed as
122 // tool_output, errors included, so adding tool_error would list them twice.
123 req.Kinds = []string{"user_text", "assistant_text", "tool_input", "tool_output"}
124 }
125 sort.Strings(req.Kinds)
126 key := req
127 key.Cursor, key.Limit = "", 0
128 active := a.activeSessionPath(a.activeSessionDir())
129 activeBinding := active
130 if targetPath != "" {
131 // Exact-target reads belong to the selected source, not foreground focus.
132 activeBinding = ""
133 }
134 binding := snapshotBinding("history-search", []any{key, targetPath, activeBinding})
135 store := &a.desktopSessions.readSnapshots
136 var first *readSnapshot
137 var err error
138 if req.Cursor == "" {
139 first, err = store.build(a.bootContext(), binding, func(ctx context.Context, snap *readSnapshot) error {
140 return a.buildHistorySearchSnapshot(ctx, store, snap, req, targetPath, active)
141 })
142 }
143 if err == nil {
144 var meta json.RawMessage
145 out.NextCursor, out.SnapshotID, out.SnapshotExpiresAt, meta, err = store.page(a.bootContext(), binding, req.Cursor, first, req.Limit, func(b []byte) error {
146 var row HistorySearchHit
147 if err := json.Unmarshal(b, &row); err != nil {
148 return err
149 }
150 out.Items = append(out.Items, row)
151 return nil
152 })
153 if err == nil {
154 var frozen HistorySearchPage
155 err = json.Unmarshal(meta, &frozen)
156 out.Revision, out.Partial, out.Status = frozen.Revision, frozen.Partial, frozen.Status
157 }
158 }
159 if err != nil {
160 out = HistorySearchPage{Items: []HistorySearchHit{}}
161 out.ReadError, out.StaleCursor = snapshotReadError(err)
162 out.Status.LastError = err.Error()
163 }
164 return out
165 }
166
166 lines GO