返回 DeepSeek-Reasonix
session_workspace_sidebar.go
根目录 / desktop / session_workspace_sidebar.go
1 package main
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "os"
8 "strings"
9 "time"
10
11 "reasonix/desktop/internal/workspacestate"
12 "reasonix/internal/session"
13 )
14
15 // Both recovery and the sidebar resolve durable registry members. Legacy
16 // catalog rows remain available only until their exact source is adopted.
17 func (a *App) unifiedProjectTopics(req ProjectTopicPageRequest) (ProjectTopicPage, error) {
18 return a.readProjectTopicPage(req, a.desktopSessionService("").Query())
19 }
20
21 func (a *App) readProjectTopicPage(req ProjectTopicPageRequest, reader workspaceSessionInfoReader) (ProjectTopicPage, error) {
22 scope, root, err := normalizeOrganizationTarget(req.Scope, req.WorkspaceRoot)
23 if err != nil {
24 return ProjectTopicPage{Items: []ProjectNode{}}, err
25 }
26 req.Scope, req.WorkspaceRoot = scope, root
27 identity := req
28 identity.Cursor, identity.Limit = "", 0
29 binding := snapshotBinding("project-topics", []any{identity, req.pinnedOnly})
30 store := &a.desktopSessions.readSnapshots
31 var first *readSnapshot
32 if req.Cursor == "" {
33 first, err = store.build(a.bootContext(), binding, func(ctx context.Context, snap *readSnapshot) error {
34 page, validate, err := a.buildProjectTopics(req, reader, snap)
35 if err != nil {
36 return err
37 }
38 snap.validate = validate
39 for _, row := range page.Items {
40 if err := store.reserve(snap, 512); err != nil {
41 return err
42 }
43 if err := store.append(ctx, snap, row); err != nil {
44 return err
45 }
46 }
47 page.Items = nil
48 snap.metadata, err = json.Marshal(page)
49 return err
50 })
51 if err != nil {
52 return ProjectTopicPage{Items: []ProjectNode{}}, err
53 }
54 }
55 items := []ProjectNode{}
56 next, id, expires, meta, err := store.page(a.bootContext(), binding, req.Cursor, first, req.Limit, func(b []byte) error {
57 var row ProjectNode
58 if err := json.Unmarshal(b, &row); err != nil {
59 return err
60 }
61 items = append(items, row)
62 return nil
63 })
64 if err != nil {
65 return ProjectTopicPage{Items: []ProjectNode{}}, err
66 }
67 var out ProjectTopicPage
68 if err := json.Unmarshal(meta, &out); err != nil {
69 return ProjectTopicPage{Items: []ProjectNode{}}, err
70 }
71 out.Items, out.NextCursor, out.SnapshotID, out.SnapshotExpiresAt = items, next, id, expires
72 return out, nil
73 }
74
75 func (a *App) buildProjectTopics(req ProjectTopicPageRequest, reader workspaceSessionInfoReader, snap *readSnapshot) (ProjectTopicPage, func() error, error) {
76 scope, root := req.Scope, req.WorkspaceRoot
77 workspaceID, _, err := a.ensureSessionOrganization(scope, root)
78 if err != nil {
79 return ProjectTopicPage{Items: []ProjectNode{}}, nil, err
80 }
81 state, versions, err := a.workspaceRegistry().LoadProjectionWithVersions(a.bootContext())
82 if err != nil {
83 return ProjectTopicPage{Items: []ProjectNode{}}, nil, err
84 }
85 workspace := state.Workspaces[workspaceID]
86 org := workspacestate.Organization{}
87 if workspace.Organization != nil {
88 org = *workspace.Organization
89 }
90 if page, validate, handled, err := a.lazyProjectTopicSnapshot(req, reader, snap, state, workspaceID, org, versions); handled {
91 return page, validate, err
92 }
93 return a.materializeProjectTopics(req, reader, snap, state, workspacestate.NewWorkspaceIndex(state), workspaceID, org, nil, versions)
94 }
95
96 // projectTopicsFromProjection materializes one workspace from a caller-owned
97 // registry snapshot. Project-tree reads use it without organization migration
98 // or repeated registry loads, so building sidebar shells remains read-only.
99 func (a *App) projectTopicsFromProjection(req ProjectTopicPageRequest, state workspacestate.State, workspaceIndex *workspacestate.WorkspaceIndex, workspaceID string, org workspacestate.Organization, shellPreferences *desktopProject, snapshotVersions ...*workspacestate.ReadVersions) (ProjectTopicPage, error) {
100 scope, root, err := normalizeOrganizationTarget(req.Scope, req.WorkspaceRoot)
101 if err != nil {
102 return ProjectTopicPage{Items: []ProjectNode{}}, err
103 }
104 req.Scope, req.WorkspaceRoot = scope, root
105 identity := req
106 identity.Cursor, identity.Limit = "", 0
107 binding := snapshotBinding("project-topics-projection", []any{identity, req.pinnedOnly, state.Generation, org.Revision, shellPreferences})
108 store := &a.desktopSessions.readSnapshots
109 var first *readSnapshot
110 if req.Cursor == "" {
111 first, err = store.build(a.bootContext(), binding, func(ctx context.Context, snap *readSnapshot) error {
112 versions := projectionReadVersions(state, snapshotVersions)
113 effectiveOrg := org
114 tryLazy := true
115 if shellPreferences != nil {
116 workspace := state.Workspaces[workspaceID]
117 if workspace.Organization != nil {
118 effectiveOrg = *workspace.Organization
119 }
120 // An old manual order needs its source-alias translation first.
121 tryLazy = effectiveOrg.MigrationVersion != 0 || !shellPreferences.ManualSessionOrder && !shellPreferences.ManualTopicOrder
122 }
123 var page ProjectTopicPage
124 var validate func() error
125 var handled bool
126 var err error
127 if tryLazy {
128 page, validate, handled, err = a.lazyProjectTopicSnapshot(req, a.desktopSessionService("").Query(), snap, state, workspaceID, effectiveOrg, versions)
129 }
130 if !handled {
131 page, validate, err = a.materializeProjectTopics(req, a.desktopSessionService("").Query(), snap, state, workspaceIndex, workspaceID, org, shellPreferences, versions)
132 }
133 if err != nil {
134 return err
135 }
136 snap.validate = validate
137 for _, row := range page.Items {
138 if err := store.reserve(snap, 512); err != nil {
139 return err
140 }
141 if err := store.append(ctx, snap, row); err != nil {
142 return err
143 }
144 }
145 page.Items = nil
146 snap.metadata, err = json.Marshal(page)
147 return err
148 })
149 if err != nil {
150 return ProjectTopicPage{Items: []ProjectNode{}}, err
151 }
152 }
153 items := []ProjectNode{}
154 next, id, expires, meta, err := store.page(a.bootContext(), binding, req.Cursor, first, req.Limit, func(b []byte) error {
155 var row ProjectNode
156 if err := json.Unmarshal(b, &row); err != nil {
157 return err
158 }
159 items = append(items, row)
160 return nil
161 })
162 if err != nil {
163 return ProjectTopicPage{Items: []ProjectNode{}}, err
164 }
165 var out ProjectTopicPage
166 if err := json.Unmarshal(meta, &out); err != nil {
167 return ProjectTopicPage{Items: []ProjectNode{}}, err
168 }
169 out.Items, out.NextCursor, out.SnapshotID, out.SnapshotExpiresAt = items, next, id, expires
170 return out, nil
171 }
172
173 func (a *App) materializeProjectTopics(req ProjectTopicPageRequest, reader workspaceSessionInfoReader, snap *readSnapshot, state workspacestate.State, workspaceIndex *workspacestate.WorkspaceIndex, workspaceID string, org workspacestate.Organization, shellPreferences *desktopProject, versions *workspacestate.ReadVersions) (ProjectTopicPage, func() error, error) {
174 workspace := state.Workspaces[workspaceID]
175 if err := applyOrganizationGroupFilter(&req, org); err != nil {
176 return ProjectTopicPage{Items: []ProjectNode{}}, nil, err
177 }
178 workspace.SessionIDs = admittedWorkspaceTopicMembers(req, state, workspace)
179 infos, _ := listWorkspaceSessionInfo(a.bootContext(), reader, workspace.SessionIDs)
180 adopted := adoptedSourceRows(state)
181 adoptedTopics := state.AdoptedTopicIDs(workspaceID)
182 all := req
183 all.Cursor = ""
184 all.Query = ""
185 all.TimeFilter = ""
186 all.GroupFilter = "all"
187 all.ExcludePinned = false
188 all.groupSelected = nil
189 all.groupAll = nil
190 legacy, err := a.unadoptedLegacyTopics(all, adopted, adoptedTopics)
191 if err != nil {
192 return legacy, nil, err
193 }
194 legacy.Items = a.withRemovablePlaceholderTopics(req, state, legacy.Items, adoptedTopics)
195 sources := append(legacy.Items, a.historicalCanonicalTopicsFromProjection(req.Scope, req.WorkspaceRoot, state, workspaceIndex)...)
196 if saved, err := readHistoricalSidecar(); err == nil {
197 applyHistoricalPresentations(sources, saved)
198 }
199 if shellPreferences != nil {
200 org = projectedShellOrganization(workspace, state, sources, *shellPreferences)
201 }
202 filtered := a.indexedWorkspaceTopics(req, state, workspace, org, infos, sources)
203 legacy.Items = cloneTopicPage(filtered)
204 for i := range legacy.Items {
205 if ref := legacy.Items[i].Session; ref != nil {
206 _, legacy.Items[i].Open = a.desktopSessionService("").Runtime(*ref)
207 }
208 }
209 legacy.Revision += state.Generation
210 legacy.NextCursor = ""
211 // Ordinary activity and organization edits do not revoke frozen reads.
212 // Membership removal, lifecycle transitions and source adoption do.
213 validate := a.workspaceReadFence(versions, workspace, filtered)
214 sourcesFence := &readSourceFence{app: a, files: map[string]os.FileInfo{}, bindings: map[string]string{}, versions: versions, store: &a.desktopSessions.readSnapshots, snapshot: snap, metadataOnly: true}
215 for _, node := range filtered {
216 if node.Session != nil {
217 continue
218 }
219 path := node.SessionPath
220 if node.Source != nil {
221 path = node.Source.Path
222 }
223 if path != "" {
224 if err := sourcesFence.add(a.bootContext(), path); err != nil {
225 return legacy, nil, err
226 }
227 }
228 }
229 return legacy, func() error {
230 current, err := a.workspaceRegistry().VerifySnapshot(a.bootContext())
231 if err != nil {
232 return err
233 }
234 if err := validate(current); err != nil {
235 return err
236 }
237 return sourcesFence.validateWithCurrent(current)
238 }, nil
239 }
240
241 func desktopSessionTimeCutoff(filter string) int64 {
242 value := strings.ToLower(strings.TrimSpace(filter))
243 switch value {
244 case "day":
245 value = "24h"
246 case "week", "7d":
247 value = "168h"
248 case "month", "30d":
249 value = "720h"
250 }
251 duration, err := time.ParseDuration(value)
252 if err != nil || duration <= 0 {
253 return 0
254 }
255 return time.Now().Add(-duration).UnixMilli()
256 }
257
258 func projectTopicTimeCutoff(req ProjectTopicPageRequest) int64 {
259 if req.timeCutoff > 0 {
260 return req.timeCutoff
261 }
262 return desktopSessionTimeCutoff(req.TimeFilter)
263 }
264
265 func (a *App) unifiedProjectRevision(catalogRevision uint64) uint64 {
266 state, err := a.workspaceRegistry().LoadProjection(a.bootContext())
267 if err != nil {
268 return catalogRevision
269 }
270 return catalogRevision + state.Generation
271 }
272
273 func (a *App) updateCanonicalTopicPresentation(topicID string, title *string, pinned *bool) (bool, error) {
274 state, err := a.workspaceRegistry().LoadProjection(a.bootContext())
275 if err != nil {
276 return false, err
277 }
278 ids := []string{}
279 for _, workspace := range state.Workspaces {
280 for _, id := range workspace.SessionIDs {
281 if state.Presentation[id].TopicID == topicID || "canonical-"+id == topicID {
282 ids = append(ids, id)
283 }
284 }
285 }
286 if len(ids) == 0 {
287 return false, nil
288 }
289 if title != nil {
290 for _, id := range ids {
291 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: id}
292 if err := a.desktopSessionService("").SetTitle(a.bootContext(), ref, *title); err != nil {
293 return true, err
294 }
295 a.publishCanonicalSessionTitle(ref, *title)
296 }
297 }
298 if pinned != nil {
299 if err := a.workspaceRegistry().UpdatePresentation(a.bootContext(), ids, nil, pinned); err != nil {
300 return true, err
301 }
302 a.emitProjectTreeMetadataChanged()
303 }
304 return true, nil
305 }
306
307 func (a *App) mergeCanonicalWorkspaceShells(projects []ProjectNode) []ProjectNode {
308 state, versions, err := a.workspaceRegistry().LoadProjectionWithVersions(a.bootContext())
309 if err != nil {
310 return projects
311 }
312 return a.mergeCanonicalWorkspaceShellsFromProjection(projects, state, versions)
313 }
314
315 func (a *App) mergeCanonicalWorkspaceShellsFromProjection(projects []ProjectNode, state workspacestate.State, snapshotVersions ...*workspacestate.ReadVersions) []ProjectNode {
316 versions := projectionReadVersions(state, snapshotVersions)
317 preferences := loadProjectsFile()
318 workspaceIndex := workspacestate.NewWorkspaceIndex(state)
319 owner := func(scope, root string) (string, bool) {
320 if scope != "project" {
321 _, found := state.Workspaces[workspacestate.GlobalWorkspaceID]
322 return workspacestate.GlobalWorkspaceID, found
323 }
324 id, found, err := workspaceIndex.Resolve(root)
325 return id, found && err == nil
326 }
327 present := map[string]bool{}
328 visible := make([]ProjectNode, 0, len(projects))
329 for _, project := range projects {
330 if project.Remote != nil {
331 visible = append(visible, project)
332 continue
333 }
334 scope := "project"
335 if project.Kind == "global_folder" {
336 scope = "global"
337 }
338 id, found := owner(scope, project.Root)
339 if workspace, ok := state.Workspaces[id]; found && ok && !workspace.Visible {
340 continue
341 }
342 if found {
343 present[id] = true
344 }
345 visible = append(visible, project)
346 }
347 projects = visible
348 for _, id := range state.WorkspaceIDs {
349 workspace := state.Workspaces[id]
350 if !workspace.Visible || present[id] {
351 continue
352 }
353 if id == workspacestate.GlobalWorkspaceID && !globalWorkspaceHasSessions(state) {
354 continue
355 }
356 kind, key := "project", "project_"+workspace.Root
357 if id == workspacestate.GlobalWorkspaceID {
358 kind, key = "global_folder", "global_folder"
359 }
360 projects = append(projects, ProjectNode{Key: key, Kind: kind, Root: workspace.Root, Label: workspace.Title, Children: []ProjectNode{}})
361 }
362 // Pinned shells must use the same canonical rows as ordinary pages. Legacy
363 // shells otherwise overwrite the title/key on refresh and resurrect pins
364 // for sessions whose registry lifecycle is already archived.
365 for index := range projects {
366 project := &projects[index]
367 if project.Remote != nil {
368 continue
369 }
370 scope, root := "project", project.Root
371 if project.Kind == "global_folder" {
372 scope, root = "global", ""
373 }
374 req := ProjectTopicPageRequest{Scope: scope, WorkspaceRoot: root, Limit: 200, pinnedOnly: true}
375 workspaceID, found := owner(scope, root)
376 workspace := state.Workspaces[workspaceID]
377 legacy := legacyOrganizationPreferences(preferences, scope, root)
378 if !found || len(workspace.SessionIDs) == 0 {
379 pins, err := a.historicalPinnedShellsFromProjection(req, state, workspaceIndex, legacy)
380 if err != nil {
381 project.Health = "metadata_failed"
382 } else {
383 project.Children = pins
384 }
385 continue
386 }
387 pins := []ProjectNode{}
388 snapshotID := ""
389 for {
390 page, err := a.projectTopicsFromProjection(req, state, workspaceIndex, workspaceID, workspacestate.Organization{}, &legacy, versions)
391 if err != nil {
392 project.Health = "metadata_failed"
393 break
394 }
395 if req.Cursor == "" {
396 snapshotID = page.SnapshotID
397 }
398 unpinned := false
399 for _, node := range page.Items {
400 if node.Pinned {
401 pins = append(pins, node)
402 } else {
403 unpinned = true
404 }
405 }
406 if unpinned || page.NextCursor == "" {
407 project.Children = pins
408 break
409 }
410 req.Cursor = page.NextCursor
411 }
412 a.ReleaseReadSnapshot(snapshotID)
413 }
414 return projects
415 }
416
417 // globalWorkspaceHasSessions decides whether the registry alone keeps Global
418 // in the sidebar. Global is never added by the user: every draft or session
419 // aimed at it marks it Visible, so Visible without a session is not a request.
420 func globalWorkspaceHasSessions(state workspacestate.State) bool {
421 for _, sessionID := range state.Workspaces[workspacestate.GlobalWorkspaceID].SessionIDs {
422 if lifecycle := state.SessionStates[sessionID].Lifecycle; lifecycle != workspacestate.Archived && lifecycle != workspacestate.Deleted {
423 return true
424 }
425 }
426 for _, mapping := range state.SourceMappings {
427 if mapping.WorkspaceID == workspacestate.GlobalWorkspaceID {
428 return true
429 }
430 }
431 return false
432 }
433
434 func (a *App) unadoptedLegacyTopics(req ProjectTopicPageRequest, adopted, adoptedTopics map[string]bool) (ProjectTopicPage, error) {
435 if req.metadataSnapshot == nil {
436 rows := a.metadataProjectTopics(req.Scope, req.WorkspaceRoot)
437 req.metadataSnapshot = &rows
438 }
439 req.readAllSources = true
440 if catalog := a.sessionCatalog.Load(); catalog != nil && req.readContext == nil {
441 availability := a.catalogWorkspaceAvailability(catalog, req.Scope, req.WorkspaceRoot)
442 req.readAvailability = &availability
443 var page ProjectTopicPage
444 err := catalog.WithReadView(a.bootContext(), func(ctx context.Context) error {
445 req.readContext = ctx
446 var err error
447 page, err = a.unadoptedLegacyTopics(req, adopted, adoptedTopics)
448 return err
449 })
450 return page, err
451 }
452 legacyReq := req
453 legacyReq.Cursor, legacyReq.Limit = "", 200
454 legacy := ProjectTopicPage{Items: []ProjectNode{}}
455 deleted := map[string]bool{}
456 for _, topicID := range loadProjectsFile().DeletedTopics {
457 deleted[topicID] = true
458 }
459 for {
460 page, err := a.listProjectTopics(legacyReq)
461 if err != nil {
462 return page, err
463 }
464 legacy.Complete, legacy.ReadyDirectories, legacy.PendingDirectories, legacy.FailedDirectories = page.Complete, page.ReadyDirectories, page.PendingDirectories, page.FailedDirectories
465 legacy.Revision = max(legacy.Revision, page.Revision)
466 expanded := []ProjectNode{}
467 for _, node := range page.Items {
468 if deleted[node.TopicID] {
469 continue
470 }
471 rows := expandSessionSourceRows(node)
472 // A single visible head is also addressed by its historical path.
473 // Preserve explicit sibling identities once the source has branched.
474 if len(rows) == 1 && rows[0].Source != nil && adopted[sessionRuntimeKey(node.SessionPath)] {
475 continue
476 }
477 expanded = append(expanded, rows...)
478 }
479 for _, node := range expanded {
480 if node.Source != nil {
481 if a.unavailableHistoricalSource(node.Source.SourceKey) {
482 continue
483 }
484 node.PreparationStatus = a.historicalPreparationStatus(node.Source.SourceKey)
485 if !adopted[projectNodeSessionKey(node)] {
486 legacy.Items = append(legacy.Items, node)
487 }
488 continue
489 }
490 if adopted[sessionRuntimeKey(node.SessionPath)] || (node.SessionPath == "" && adoptedTopics[node.TopicID]) {
491 remaining := []ProjectNode{}
492 for _, child := range node.Children {
493 if !adopted[sessionRuntimeKey(child.SessionPath)] {
494 remaining = append(remaining, child)
495 }
496 }
497 if len(remaining) > 0 {
498 node.Children = remaining
499 node.SessionPath = remaining[0].SessionPath
500 legacy.Items = append(legacy.Items, node)
501 }
502 continue
503 }
504 legacy.Items = append(legacy.Items, node)
505 }
506 if page.NextCursor == "" {
507 break
508 }
509 if page.NextCursor == legacyReq.Cursor {
510 return legacy, fmt.Errorf("legacy session cursor did not advance")
511 }
512 legacyReq.Cursor = page.NextCursor
513 }
514 return legacy, nil
515 }
516
517 func (a *App) canonicalTopicNodes(req ProjectTopicPageRequest, state workspacestate.State, workspace workspacestate.Workspace, infos map[string]session.SessionInfo, initial []ProjectNode, created ...map[string]int64) []ProjectNode {
518 workspaceID := workspace.ID
519 service := a.desktopSessionService("")
520 nodes := initial
521 query := strings.ToLower(strings.TrimSpace(req.Query))
522 cutoff := projectTopicTimeCutoff(req)
523 var createdTopics map[string]int64
524 if len(created) > 0 {
525 createdTopics = created[0]
526 } else {
527 createdTopics = loadTopicCreatedAts(topicTitleRoot(req.Scope, req.WorkspaceRoot))
528 }
529 for index, id := range workspace.SessionIDs {
530 if state.SessionStates[id].Lifecycle != workspacestate.Active {
531 continue
532 }
533 info, found := infos[id]
534 if !found || info.MetadataStatus == session.MetadataFailed {
535 continue
536 }
537 row := workspaceSessionRow(workspaceID, id, info, found, false, service)
538 if found && cutoff > 0 && max(row.CreatedAt, row.UpdatedAt) < cutoff {
539 continue
540 }
541 if query != "" && !strings.Contains(strings.ToLower(row.Title+"\n"+row.Preview+"\n"+id), query) {
542 continue
543 }
544 ref := session.SessionRef{HostID: localDesktopHostID, SessionID: id}
545 presentation := state.Presentation[id]
546 label := a.localizedTopicTitle(sessionDisplayTitle(info, presentation))
547 kind := "topic"
548 if req.Scope != "project" {
549 kind = "global_topic"
550 }
551 topicID := presentation.TopicID
552 if topicID == "" {
553 topicID = "canonical-" + id
554 }
555 sortOrder := index
556 createdAt := row.CreatedAt
557 if previous := createdTopics[topicID]; previous > 0 {
558 createdAt = previous
559 }
560 node := ProjectNode{
561 Key: "canonical_" + id, Kind: kind, Label: label, Root: workspace.Root,
562 TopicID: topicID, Session: &ref, SessionPath: sessionRoute(id), CanArchive: row.Health != "missing",
563 Preview: row.Preview, Turns: row.Turns, TurnsState: row.MetadataStatus, Health: row.Health,
564 CreatedAt: createdAt, LastActivityAt: row.UpdatedAt, ResultSequence: row.ResultSequence, Open: row.Running,
565 Pinned: presentation.Pinned, SortOrder: sortOrder, Children: []ProjectNode{},
566 }
567 if row.ParentSessionID != "" {
568 node.ParentSession = &session.SessionRef{HostID: localDesktopHostID, SessionID: row.ParentSessionID}
569 }
570 node.SessionOrigin = row.Origin
571 if projectNodeRequestAllows(req, node) {
572 nodes = append(nodes, node)
573 }
574 }
575 return nodes
576 }
577
578 func filterWorkspaceSessionNodes(req ProjectTopicPageRequest, org workspacestate.Organization, state workspacestate.State, workspaceID string, nodes []ProjectNode) []ProjectNode {
579 ranks := map[string]int{}
580 for i, key := range org.Order {
581 ranks[key] = i
582 }
583 filtered := []ProjectNode{}
584 seen := map[string]bool{}
585 cutoff := projectTopicTimeCutoff(req)
586 query := strings.ToLower(strings.TrimSpace(req.Query))
587 aliases := workspaceSourceAliases(state, workspaceID)
588 for _, n := range nodes {
589 if req.pinnedOnly && !n.Pinned {
590 continue
591 }
592 key := projectNodeSessionKey(n)
593 if seen[key] {
594 continue
595 }
596 seen[key] = true
597 n.SortOrder = -1
598 if org.ManualOrderEnabled {
599 if rank, ok := ranks[key]; ok {
600 n.SortOrder = rank
601 }
602 }
603 if n.Session != nil {
604 n.IdentityAliases = aliases[n.Session.SessionID]
605 n.LifecycleGeneration = state.SessionStates[n.Session.SessionID].Generation
606 }
607 if !projectNodeRequestAllows(req, n) || cutoff > 0 && max(n.CreatedAt, n.LastActivityAt) < cutoff {
608 continue
609 }
610 if query != "" && !strings.Contains(strings.ToLower(n.Label+"\n"+n.Preview+"\n"+key), query) {
611 continue
612 }
613 filtered = append(filtered, n)
614 }
615 return filtered
616 }
617
617 lines GO