okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5e287ce
main
1package tui 2 3import ( 4"albedo/cli/internal/daemon" 5"context" 6"errors" 7"maps" 8"slices" 9"time" 10 11 tea"charm.land/bubbletea/v2" 12) 13 14func (m * AppModel )handleSessionResults (msg tea.Msg ) (tea.Cmd ,bool ) { 15switch msg := msg .(type ) { 16case SessionPreviewMsg : 17if msg .Err == nil && msg .Preview .Session != nil { 18current ,known := m .session (msg .ID ) 19if known && current .ETag == msg .ExpectedETag { 20m .updateSession (* msg .Preview .Session ) 21 } 22 } 23var cmd tea.Cmd 24m .SessionPicker ,cmd = m .SessionPicker .Update (msg ) 25return cmd ,true 26 27case sessionDeletedMsg : 28if msg .Err != nil { 29m .AddError (operationError (msg .Err ,"Could not delete the session: " ,"Session may have been deleted; refresh the session list before trying again." )) 30return nil ,true 31 } 32m .ClearNotices () 33if msg .Result == nil { 34return nil ,true 35 } 36deleted := slices .Clone (msg .Result .DeletedIDs ) 37if msg .Result .OK && ! slices .Contains (deleted ,msg .ID ) { 38deleted = append (deleted ,msg .ID ) 39 } 40for _ ,id := range deleted { 41delete (m .pendingBySession ,id ) 42m .SessionPicker .ForgetPreview (id ) 43 } 44m .Sessions = slices .DeleteFunc (m .Sessions ,func (s daemon.Session )bool {return slices .Contains (deleted ,s .ID ) }) 45m .updateSessionPickerItems () 46if ! msg .Result .OK || msg .Result .Truncated { 47m .AddNotice (msg .Result .Message ) 48m .SessionPicker .notice = msg .Result .Message 49m .SessionGen ++ 50return m .loadSessionsCmd (m .SessionGen ,msg .Result .Message ),true 51 } 52return nil ,true 53case SessionRenameMsg : 54m .SessionGen ++ 55return m .renameSessionCmd (msg ),true 56case sessionRenamedMsg : 57var cmd tea.Cmd 58if m .State == AppStateAgents { 59m .Agents ,cmd = m .Agents .Update (msg ) 60 } 61if msg .Err != nil { 62m .SessionPicker .notice = operationError (msg .Err ,"Could not rename the session: " ,"Session may have been renamed; refresh the session list before trying again." ) 63return cmd ,true 64 } 65m .updateSession (msg .Session ) 66m .SessionPicker .notice = "" 67return cmd ,true 68case sessionsLoadedMsg : 69if msg .Gen != m .SessionGen { 70return nil ,true 71 } 72m .listingSessions = false 73if msg .Err != nil { 74m .SessionPicker .Loading = false 75m .AddError ("Could not load sessions: " + msg .Err .Error ()) 76return nil ,true 77 } 78m .applySessionList (msg .Sessions ) 79 80m .ClearNotices () 81if msg .Notice != "" { 82m .AddNotice (msg .Notice ) 83m .SessionPicker .notice = msg .Notice 84 } 85return m .SessionPicker .PreviewCmd (),true 86case sessionCreationRecoveryMsg : 87if pending ,exists := m .pendingCreations [msg .Handle .ID ()];! exists || pending .Expired { 88return nil ,true 89 } 90conn ,handle ,gen := m .Conn ,msg .Handle ,msg .Gen 91return func () tea.Msg { 92session ,err := daemon .ResolveCreation (context .Background (),conn ,handle ) 93return sessionCreatedMsg {Session :session ,Err :err ,Handle :handle ,Gen :gen } 94 },true 95case sessionCreatedMsg : 96if msg .Gen != m .SessionGen && (msg .Handle == nil || m .pendingCreations [msg .Handle .ID ()].Handle == nil ) { 97return nil ,true 98 } 99if msg .Handle != nil && daemon .IsOperationExpired (msg .Err ) { 100pending := m .pendingCreations [msg .Handle .ID ()] 101pending .Expired = true 102m .pendingCreations [msg .Handle .ID ()]= pending 103m .AddError ("Session operation " + msg .Handle .ID ()+ " expired; its outcome is unresolved." ) 104return nil ,true 105 } 106if msg .Err != nil && msg .Handle != nil { 107if _ ,uncertain := errors.AsType [* daemon.UncertainOutcomeError ](msg .Err );uncertain { 108m .AddError ("Session admission is uncertain. Checking operation " + msg .Handle .ID ()+ "." ) 109return creationRecoveryCmd (msg .Handle ,msg .Gen ),true 110 } 111if api ,ok := errors.AsType [* daemon.APIError ](msg .Err );ok && (api .Code == "operation_unknown" || api .StatusCode >= 500 ) { 112return creationRecoveryCmd (msg .Handle ,msg .Gen ),true 113 } 114 } 115if msg .Handle != nil { 116delete (m .pendingCreations ,msg .Handle .ID ()) 117 } 118if msg .Gen != m .SessionGen { 119if msg .Err != nil { 120m .AddError (msg .Err .Error ()) 121return nil ,true 122 } 123if ! slices .ContainsFunc (m .Sessions ,func (session daemon.Session )bool {return session .ID == msg .Session .ID }) { 124m .Sessions = append (m .Sessions ,msg .Session ) 125 } 126m .updateSessionPickerItems () 127m .ClearNotices () 128m .AddNotice ("Recovered the created session " + msg .Session .ID ) 129return nil ,true 130 } 131if msg .Err != nil && m .State == AppStateFolderPicker { 132m .FolderPicker .Refused (errors .New (operationError (msg .Err ,"" ,"Session may have been created; check the session list before creating another." ))) 133return nil ,true 134 } 135if msg .Err != nil { 136m .AddError (operationError (msg .Err ,"Could not create a session: " ,"Session may have been created; check the session list before creating another." )) 137return nil ,true 138 } 139return m .setChatSession (msg .Session ,true ),true 140 } 141return nil ,false 142} 143 144func (m * AppModel )loadSessionsCmd (gen int ,notices ... string ) tea.Cmd { 145m .listingSessions = true 146conn := m .Conn 147notice := "" 148if len (notices )> 0 { 149notice = notices [0 ] 150 } 151return func () tea.Msg { 152if conn == nil { 153return sessionsLoadedMsg {Err :errors .New ("daemon connection unavailable" ),Gen :gen } 154 } 155sessions ,err := daemon .ListSessions (context .Background (),conn ) 156return sessionsLoadedMsg {Sessions :sessions ,Err :err ,Gen :gen ,Notice :notice } 157 } 158} 159 160func (m * AppModel )renameSessionCmd (rename SessionRenameMsg ) tea.Cmd { 161conn := m .Conn 162return func () tea.Msg { 163if conn == nil { 164return sessionRenamedMsg {SessionRenameMsg :rename ,Err :errors .New ("daemon connection unavailable" )} 165 } 166etag := rename .ETag 167if etag == "" { 168current ,err := currentCondition (conn ,rename .ID ) 169if err != nil { 170return sessionRenamedMsg {SessionRenameMsg :rename ,Err :err } 171 } 172etag = current .ETag 173 } 174s ,err := daemon .RenameSession (context .Background (),conn ,rename .ID ,rename .Name ,etag ) 175return sessionRenamedMsg {SessionRenameMsg :rename ,Session :s ,Err :err } 176 } 177} 178 179func (m * AppModel )deleteSessionCmd (id string ,condition daemon.SessionCondition ) tea.Cmd { 180m .SessionGen ++ 181conn := m .Conn 182return func () tea.Msg { 183if conn == nil { 184return sessionDeletedMsg {ID :id ,Err :errors .New ("daemon connection unavailable" )} 185 } 186if condition .ETag == "" || condition .FamilyRevision == "" { 187current ,err := currentCondition (conn ,id ) 188if err != nil { 189return sessionDeletedMsg {ID :id ,Err :err } 190 } 191condition = current 192 } 193result ,err := daemon .DeleteSession (context .Background (),conn ,id ,true ,condition ) 194return sessionDeletedMsg {ID :id ,Err :err ,Result :& result } 195 } 196} 197 198// currentCondition reads validators missing from a selected session row. 199func currentCondition (conn * daemon.Connection ,id string ) (daemon.SessionCondition ,error ) { 200current ,err := daemon .GetSessionConfiguration (context .Background (),conn ,id ) 201return daemon.SessionCondition {ETag :current .ETag ,FamilyRevision :current .Value .FamilyRevision },err 202} 203 204func sessionPreviewCmd (conn * daemon.Connection ,id ,etag string ) tea.Cmd { 205return func () tea.Msg { 206preview ,err := daemon .PreviewHistory (context .Background (),conn ,id ,16 ) 207return SessionPreviewMsg {ID :id ,ExpectedETag :etag ,Preview :preview ,Err :err } 208 } 209} 210 211func (m * AppModel )createSessionCmd (gen int ,workspace string ) tea.Cmd { 212conn := m .Conn 213handle ,preparationErr := daemon .NewCreation (daemon.CreateSessionRequest {Workspace :workspace }) 214if preparationErr == nil { 215if m .pendingCreations == nil { 216m .pendingCreations = make (map [string ]pendingOperation ) 217 } 218m .pendingCreations [handle .ID ()]= pendingOperation {Handle :handle } 219 } 220return func () tea.Msg { 221if preparationErr != nil { 222return sessionCreatedMsg {Err :preparationErr ,Gen :gen } 223 } 224if conn == nil { 225return sessionCreatedMsg {Err :errors .New ("daemon connection unavailable" ),Gen :gen ,Handle :handle } 226 } 227session ,err := daemon .CreateSessionOperation (context .Background (),conn ,handle ) 228return sessionCreatedMsg {Session :session ,Err :err ,Gen :gen ,Handle :handle } 229 } 230} 231 232func creationRecoveryCmd (handle * daemon.OperationHandle ,gen int ) tea.Cmd { 233return tea .Tick (15 * time .Second ,func (time.Time ) tea.Msg {return sessionCreationRecoveryMsg {Handle :handle ,Gen :gen } }) 234} 235 236func (m * AppModel )updateSessionPickerItems () { 237m .SessionPicker .SetSessions (m .Sessions ,m .ActiveSession ) 238} 239 240func (m * AppModel )setChatSession (s daemon.Session ,prepend bool ) tea.Cmd { 241m .SessionGen ++ 242if m .pendingBySession == nil { 243m .pendingBySession = make (map [string ]pendingSessionIntents ) 244 } 245if m .Chat .SessionID != "" { 246if len (m .Chat .pendingUsers )== 0 && len (m .Chat .pendingContinuations )== 0 { 247delete (m .pendingBySession ,m .Chat .SessionID ) 248 }else { 249m .pendingBySession [m .Chat .SessionID ]= pendingSessionIntents {users :slices .Clone (m .Chat .pendingUsers ),continuations :maps .Clone (m .Chat .pendingContinuations )} 250 } 251 } 252m .Chat .Close () 253session := s 254m .ActiveSession = & session 255if prepend && ! slices .ContainsFunc (m .Sessions ,func (listed daemon.Session )bool {return listed .ID == s .ID }) { 256m .Sessions = append ([]daemon.Session {s },m .Sessions ... ) 257 } 258m .updateSession (s ) 259m .Chat = m .newChatModel (& session ) 260m .Chat .SetSize (m .Width ,m .Height ) 261m .State = AppStateChat 262m .CatalogGen ++ 263return tea .Batch (m .Chat .Init (),m .loadCommandCatalogCmd (m .CatalogGen )) 264} 265 266// openSession makes session the chat on screen. 267func (m * AppModel )openSession (s daemon.Session ) tea.Cmd { 268m .ClearNotices () 269return tea .Batch (m .setChatSession (s ,false ),m .recordOpenCmd (s .ID )) 270} 271 272func (m * AppModel )newSessionCmd () tea.Cmd { 273m .SessionGen ++ 274return m .createSessionCmd (m .SessionGen ,m .Workspace ) 275}