| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "sort" |
| 6 | "strings" |
| 7 | |
| 8 | "reasonix/desktop/internal/workspacestate" |
| 9 | "reasonix/internal/config" |
| 10 | "reasonix/internal/session" |
| 11 | ) |
| 12 | |
| 13 | func sessionRoute(id string) string { |
| 14 | id = strings.TrimSpace(id) |
| 15 | if id == "" { |
| 16 | return "" |
| 17 | } |
| 18 | return remoteSessionIDRoutePrefix + id |
| 19 | } |
| 20 | |
| 21 | func parseSessionRoute(route string) (string, bool) { |
| 22 | locator := classifySessionLocator(route) |
| 23 | if locator.kind != sessionLocatorCanonical { |
| 24 | return "", false |
| 25 | } |
| 26 | return locator.ref.SessionID, true |
| 27 | } |
| 28 | |
| 29 | func (a *App) listCanonicalSessionsFromDir(dir, active string) []SessionMeta { |
| 30 | service := a.desktopSessionService(dir) |
| 31 | if service == nil { |
| 32 | return []SessionMeta{} |
| 33 | } |
| 34 | activeID, _ := parseSessionRoute(active) |
| 35 | state, err := a.workspaceRegistry().Load(a.bootContext()) |
| 36 | if err != nil { |
| 37 | return []SessionMeta{{Error: "workspace_registry_unavailable", TurnsState: "corrupt"}} |
| 38 | } |
| 39 | allowed := map[string]bool{} |
| 40 | for _, workspace := range state.Workspaces { |
| 41 | matches := sameDesktopPath(dir, desktopSessionDir(workspace.Root)) |
| 42 | if workspace.ID == workspacestate.GlobalWorkspaceID { |
| 43 | matches = matches || sameDesktopPath(dir, config.SessionDir()) |
| 44 | } |
| 45 | if matches { |
| 46 | for _, id := range workspace.SessionIDs { |
| 47 | if state.SessionStates[id].Lifecycle == workspacestate.Active { |
| 48 | allowed[id] = true |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | query := service.Query() |
| 54 | result := make([]SessionMeta, 0) |
| 55 | cursor := "" |
| 56 | for { |
| 57 | page, err := query.List(context.Background(), cursor, 100) |
| 58 | if err != nil { |
| 59 | return result |
| 60 | } |
| 61 | for _, info := range page.Sessions { |
| 62 | if !allowed[info.SessionID] { |
| 63 | continue |
| 64 | } |
| 65 | delete(allowed, info.SessionID) |
| 66 | meta := SessionMeta{ |
| 67 | Path: sessionRoute(info.SessionID), SessionID: info.SessionID, |
| 68 | HostID: service.HostID(), Codec: info.Codec, Error: info.Error, |
| 69 | Title: info.Title, Turns: info.Turns, TurnsState: "valid", |
| 70 | CreatedAt: info.CreatedAt.UnixMilli(), LastActivityAt: info.UpdatedAt.UnixMilli(), |
| 71 | ModTime: info.UpdatedAt.UnixMilli(), Current: info.SessionID == activeID, |
| 72 | } |
| 73 | if info.Error != "" { |
| 74 | meta.TurnsState = "corrupt" |
| 75 | } else { |
| 76 | meta.Preview = info.Preview |
| 77 | } |
| 78 | a.mu.RLock() |
| 79 | _, meta.Open = a.runtimeBySessionKey[sessionRuntimeKey(meta.Path)] |
| 80 | a.mu.RUnlock() |
| 81 | result = append(result, meta) |
| 82 | } |
| 83 | if page.NextCursor == "" { |
| 84 | break |
| 85 | } |
| 86 | cursor = page.NextCursor |
| 87 | } |
| 88 | for id := range allowed { |
| 89 | result = append(result, SessionMeta{Path: sessionRoute(id), SessionID: id, HostID: localDesktopHostID, Error: "session_content_unavailable", TurnsState: "corrupt"}) |
| 90 | } |
| 91 | sort.SliceStable(result, func(i, j int) bool { return result[i].LastActivityAt > result[j].LastActivityAt }) |
| 92 | return result |
| 93 | } |
| 94 | |
| 95 | func sessionRefForRoute(service *session.Service, route string) (session.SessionRef, bool) { |
| 96 | id, ok := parseSessionRoute(route) |
| 97 | if !ok || service == nil { |
| 98 | return session.SessionRef{}, false |
| 99 | } |
| 100 | return session.SessionRef{HostID: service.HostID(), SessionID: id}, true |
| 101 | } |
| 102 |