| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "reasonix/internal/config" |
| 5 | "reasonix/internal/sessioncatalog" |
| 6 | "sort" |
| 7 | ) |
| 8 | |
| 9 | // ListSessions returns the saved sessions newest-first for the history panel, |
| 10 | // marking the one the current conversation is writing to and attaching any |
| 11 | // user-chosen titles. |
| 12 | func (a *App) ListSessions() []SessionMeta { |
| 13 | dir := a.activeSessionDir() |
| 14 | active := a.activeSessionPath(dir) |
| 15 | if tab := a.activeTab(); tab != nil { |
| 16 | active = tab.currentSessionIdentity() |
| 17 | } |
| 18 | return a.listSessionsFromDir(dir, active) |
| 19 | } |
| 20 | |
| 21 | // ListSessionsForTab returns sessions from the directory owned by tabID. Task |
| 22 | // Monitor uses this stable target after asynchronous control lookups so a tab |
| 23 | // switch cannot redirect the eventual session lookup to another workspace. |
| 24 | func (a *App) ListSessionsForTab(tabID string) []SessionMeta { |
| 25 | target, err := a.taskMonitorTargetForTab(tabID) |
| 26 | if err != nil { |
| 27 | return []SessionMeta{} |
| 28 | } |
| 29 | active := target.sessionPath |
| 30 | if tab := a.tabByID(tabID); tab != nil { |
| 31 | active = tab.currentSessionIdentity() |
| 32 | } |
| 33 | return a.listSessionsFromDir(target.sessionDir, active) |
| 34 | } |
| 35 | |
| 36 | func (a *App) listSessionsFromDir(dir, active string) []SessionMeta { |
| 37 | v3 := a.listCanonicalSessionsFromDir(dir, active) |
| 38 | state, stateErr := a.workspaceRegistry().Load(a.bootContext()) |
| 39 | if stateErr != nil { |
| 40 | return v3 |
| 41 | } |
| 42 | scope, root := "", "" |
| 43 | if sameDesktopPath(dir, config.SessionDir()) || sameDesktopPath(dir, desktopSessionDir(globalWorkspaceRoot())) { |
| 44 | scope = "global" |
| 45 | } else { |
| 46 | for _, target := range a.sessionCatalogTargets() { |
| 47 | if sameDesktopPath(dir, target.Path) { |
| 48 | scope, root = target.Scope, target.WorkspaceRoot |
| 49 | break |
| 50 | } |
| 51 | } |
| 52 | } |
| 53 | historical := a.historicalCanonicalTopics(scope, root, state) |
| 54 | saved, _ := readHistoricalSidecar() |
| 55 | applyHistoricalPresentations(historical, saved) |
| 56 | for _, node := range historical { |
| 57 | v3 = append(v3, SessionMeta{Source: node.Source, Historical: true, PreparationStatus: node.PreparationStatus, |
| 58 | Path: node.SessionPath, Title: node.Label, TopicID: node.TopicID, Scope: scope, WorkspaceRoot: root, |
| 59 | Preview: node.Preview, Turns: node.Turns, TurnsState: node.TurnsState, |
| 60 | CreatedAt: node.CreatedAt, LastActivityAt: node.LastActivityAt, ModTime: node.LastActivityAt}) |
| 61 | } |
| 62 | adopted := map[string]bool{} |
| 63 | for _, mapping := range state.SourceMappings { |
| 64 | adopted["source\x00local\x00"+mapping.SourceKey] = true |
| 65 | if sourceMappingHasPathAlias(mapping) { |
| 66 | adopted[sessionRuntimeKey(mapping.Path)] = true |
| 67 | } |
| 68 | } |
| 69 | catalog := a.sessionCatalog.Load() |
| 70 | if catalog == nil { |
| 71 | return v3 |
| 72 | } |
| 73 | target := sessioncatalog.DirectoryTarget{Path: dir, Scope: "global"} |
| 74 | for _, candidate := range a.sessionCatalogTargets() { |
| 75 | if sameProjectRoot(candidate.Path, dir) { |
| 76 | target = candidate |
| 77 | break |
| 78 | } |
| 79 | } |
| 80 | records, err := listCatalogSessionsForDirectory(a.bootContext(), catalog, target, dir) |
| 81 | if err != nil { |
| 82 | return v3 |
| 83 | } |
| 84 | open := a.openSessionPaths(dir) |
| 85 | channelRoutes := channelSessionRoutesForDir(dir) |
| 86 | out := make([]SessionMeta, 0, len(records)+len(v3)) |
| 87 | out = append(out, v3...) |
| 88 | for _, record := range records { |
| 89 | if adopted[sessionRuntimeKey(record.Path)] { |
| 90 | continue |
| 91 | } |
| 92 | _, isOpen := open[record.Path] |
| 93 | meta := sessionMetaFromCatalog(record, record.Path == active, isOpen) |
| 94 | if route, ok := channelRoutes[sessionRuntimeKey(record.Path)]; ok { |
| 95 | applyChannelSessionRoute(&meta, route) |
| 96 | } |
| 97 | for _, row := range expandSessionSourceRows(ProjectNode{SessionPath: record.Path}) { |
| 98 | if row.Source == nil { |
| 99 | out = append(out, meta) |
| 100 | continue |
| 101 | } |
| 102 | if adopted[projectNodeSessionKey(row)] { |
| 103 | continue |
| 104 | } |
| 105 | headMeta := meta |
| 106 | headMeta.Source = row.Source |
| 107 | headMeta.Historical, headMeta.HistoricalBranch = true, row.HistoricalBranch |
| 108 | headMeta.PreparationStatus = a.historicalPreparationStatus(row.Source.SourceKey) |
| 109 | if presentation := saved.Presentations[row.Source.SourceKey]; presentation.Title != "" { |
| 110 | headMeta.Title = presentation.Title |
| 111 | } |
| 112 | headMeta.Turns, headMeta.Preview = row.Turns, row.Preview |
| 113 | if row.LastActivityAt > 0 { |
| 114 | headMeta.LastActivityAt, headMeta.ModTime = row.LastActivityAt, row.LastActivityAt |
| 115 | } |
| 116 | if row.CreatedAt > 0 { |
| 117 | headMeta.CreatedAt = row.CreatedAt |
| 118 | } |
| 119 | headMeta.Current = sessionRuntimeKey(headMeta.Path) == sessionRuntimeKey(active) |
| 120 | out = append(out, headMeta) |
| 121 | } |
| 122 | } |
| 123 | sort.SliceStable(out, func(i, j int) bool { return out[i].LastActivityAt > out[j].LastActivityAt }) |
| 124 | return out |
| 125 | } |
| 126 |