okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5924d14
main
1package tui 2 3import ( 4"albedo/cli/internal/config" 5"albedo/cli/internal/daemon" 6"context" 7"errors" 8"fmt" 9"maps" 10"slices" 11"strings" 12 13 tea"charm.land/bubbletea/v2" 14) 15 16func (m * AppModel )handleCommandResults (msg tea.Msg ) (tea.Cmd ,bool ) { 17switch msg := msg .(type ) { 18case modelChangedMsg : 19if msg .Gen != m .ModelGen || m .ActiveSession == nil || msg .SessionID != m .ActiveSession .ID { 20return nil ,true 21 } 22if msg .Selection != nil { 23selected := msg .Selection 24m .ActiveSession .Model ,m .Chat .Model = selected .Model ,selected .Model 25m .ActiveSession .Effort ,m .Chat .Effort = selected .Effort ,selected .Effort 26m .ActiveSession .Provider ,m .Chat .Provider = selected .Provider ,selected .Provider 27m .ActiveSession .Protocol = selected .Protocol 28m .ActiveSession .ETag = selected .ETag 29m .updateSession (* m .ActiveSession ) 30if selected .DefaultETag != "" { 31m .Profiles .ETag = selected .DefaultETag 32m .Profiles .Active = selected .Provider 33profile := m .Profiles .Providers [selected .Provider ] 34profile .Model = selected .Model 35profile .Effort = nil 36if selected .Effort != "" { 37profile .Effort = new (selected.Effort ) 38 } 39if m .Profiles .Providers != nil { 40m .Profiles .Providers [selected .Provider ]= profile 41 } 42 } 43if selected .ModelsETag != "" { 44if m .SettingsETags == nil { 45m .SettingsETags = map [string ]string {} 46 } 47m .SettingsETags ["models" ]= selected .ModelsETag 48 } 49m .ModelPicker .Saving = false 50if m .State == AppStateModelPicker { 51m .State = AppStateChat 52 } 53m .ClearNotices () 54 } 55if msg .Err != nil { 56message := operationError (msg .Err ,"Model settings error: " ,"Model settings may have changed; check them before trying again." ) 57if m .State == AppStateModelPicker { 58m .ModelPicker .Saving = false 59m .ModelPicker .Error = message 60 }else { 61m .AddError (message ) 62 } 63 } 64return nil ,true 65case commandExecutedMsg : 66if msg .Gen != m .CommandGen || (msg .SessionID != "" && (m .ActiveSession == nil || msg .SessionID != m .ActiveSession .ID )) { 67return nil ,true 68 } 69if msg .ModelsETag != "" { 70if m .SettingsETags == nil { 71m .SettingsETags = map [string ]string {} 72 } 73m .SettingsETags ["models" ]= msg .ModelsETag 74 } 75m .ClearNotices () 76if len (msg .Available )> 0 && m .State == AppStateChat { 77m .Chat .openEffortSelector (msg .Available ) 78return nil ,true 79 } 80if msg .Err != nil { 81m .AddError (operationError (msg .Err ,"Command error: " ,msg .Name + " may have run; check its result before running it again." )) 82return nil ,true 83 } 84if msg .Page != nil && m .State == AppStateChat && m .ActiveSession != nil { 85m .PageView = NewPageViewModel (m .Conn ,m .ActiveSession .ID ,msg .Name ) 86m .PageView .setDoc (msg .Page ) 87m .PageView .Busy = false 88m .PageView .SetSize (m .Width ,m .Height ) 89m .State = AppStatePageView 90return nil ,true 91 } 92m .AddNotice (msg .Message ) 93if msg .EffortChanged && m .ActiveSession != nil { 94m .ActiveSession .Effort = msg .Effort 95m .ActiveSession .ETag = msg .ETag 96m .Chat .Effort = msg .Effort 97m .updateSession (* m .ActiveSession ) 98 } 99return nil ,true 100case profilesLoadedMsg : 101if msg .Gen != m .ProfileGen { 102return nil ,true 103 } 104m .ClearNotices () 105if msg .Err != nil { 106m .AddError ("Could not load providers: " + msg .Err .Error ()) 107 }else { 108m .Profiles = msg .Profiles 109notice := fmt .Sprintf ("%s selected for new sessions" ,msg .Provider ) 110if m .ActiveSession != nil { 111notice += fmt .Sprintf ("; use /model to switch this session from %s" ,m .ActiveSession .Provider ) 112 } 113m .AddNotice (notice ) 114 } 115if m .StandaloneLogin { 116return tea .Quit ,true 117 } 118if msg .Err == nil && m .ActiveSession == nil && len (m .Sessions )== 0 { 119return m .newSessionCmd (),true 120 } 121if m .ActiveSession == nil { 122m .State = AppStateSessionPicker 123m .updateSessionPickerItems () 124return nil ,true 125 } 126return m .returnToChat (),true 127 } 128return nil ,false 129} 130 131func (m * AppModel )loadProfilesCmd (provider string ,gen int ) tea.Cmd { 132conn := m .Conn 133return func () tea.Msg { 134p ,err := daemon .ProviderProfiles (context .Background (),conn ) 135return profilesLoadedMsg {Profiles :p ,Provider :provider ,Err :err ,Gen :gen } 136 } 137} 138 139func (m * AppModel )loadCommandCatalogCmd (gen int ) tea.Cmd { 140conn ,hasSession := m .Conn ,m .ActiveSession != nil 141var sessionID string 142if hasSession { 143sessionID = m .ActiveSession .ID 144 } 145return func () tea.Msg { 146if conn == nil || ! hasSession { 147return commandCatalogLoadedMsg {Gen :gen } 148 } 149 150cmds ,err := daemon .ListSessionCommands (context .Background (),conn ,sessionID ) 151return commandCatalogLoadedMsg {Commands :cmds ,Err :err ,Gen :gen } 152 } 153} 154 155func (m * AppModel )changeModelCmd (model ,provider ,effort string ,raiseCap * bool ,gen int ,capKey string ) tea.Cmd { 156conn ,hasSession := m .Conn ,m .ActiveSession != nil 157var sessionID ,currentProvider ,etag string 158if hasSession { 159sessionID ,currentProvider ,etag = m .ActiveSession .ID ,m .ActiveSession .Provider ,m .ActiveSession .ETag 160 } 161modelsETag := m .SettingsETags ["models" ] 162providersETag := m .Profiles .ETag 163profiles := maps .Clone (m .Profiles .Providers ) 164saveCap := raiseCap != nil 165var capEnabled bool 166if saveCap { 167capEnabled = * raiseCap 168 } 169return func () tea.Msg { 170if conn == nil || ! hasSession { 171return modelChangedMsg {Err :errors .New ("no active session or connection" ),SessionID :sessionID ,Gen :gen } 172 } 173 174if provider == "" { 175resolved ,selected ,resolveErr := daemon .ResolveSessionModel (config.Profiles {Active :currentProvider ,Providers :profiles },model ) 176if resolveErr != nil { 177return modelChangedMsg {Err :resolveErr ,SessionID :sessionID ,Gen :gen } 178 } 179provider ,model = resolved ,selected 180 } 181selection ,err := daemon .ChangeModel (context .Background (),conn ,sessionID , daemon.ModelChangeRequest {Model :model ,Provider :provider ,Effort :effort ,ETag :etag ,DefaultETag :providersETag ,MakeDefault :true }) 182if selection .Model == "" { 183return modelChangedMsg {Err :err ,SessionID :sessionID ,Gen :gen } 184 } 185if profile ,ok := profiles [selection .Provider ];ok { 186selection .Protocol = profile .Protocol 187 } 188changed := modelChangedMsg {Selection :& selection ,SessionID :sessionID ,Gen :gen ,Err :err } 189if err != nil { 190return changed 191 } 192// Keep the confirmed switch if the following cap update fails. 193if saveCap { 194updatedETag ,err := daemon .SetModelContextCap (context .Background (),conn , daemon.ModelContextCapRequest {CapKey :capKey ,ETag :modelsETag ,Enabled :capEnabled }) 195if err != nil { 196changed .Err = fmt .Errorf ("switched model; context cap update: %w" ,err ) 197return changed 198 } 199selection .ModelsETag = updatedETag 200 } 201 202return changed 203 } 204} 205 206func (m * AppModel )executeCommandCmd (name ,args string ,gen int ) tea.Cmd { 207var captured daemon.Session 208if m .ActiveSession != nil { 209captured = * m .ActiveSession 210 } 211catalog := slices .Clone (m .CommandCatalog ) 212conn ,hasSession := m .Conn ,m .ActiveSession != nil 213modelsETag := m .SettingsETags ["models" ] 214currentProfile := profilesForSession (m .Profiles ,captured ) 215var sessionID string 216if hasSession { 217sessionID = m .ActiveSession .ID 218 } 219return func () tea.Msg { 220if conn == nil || ! hasSession { 221return commandExecutedMsg {Name :name ,Err :errors .New ("no active session or connection" ),Gen :gen } 222 } 223 224snapshot := captured 225var res daemon.CommandResult 226var err error 227var acknowledgedModelsETag string 228switch name { 229case "/effort" : 230var effort daemon.EffortResult 231if args == "" { 232effort ,err = daemon .ReadEffort (context .Background (),conn ,snapshot ) 233 }else { 234effort ,err = daemon .SelectEffort (context .Background (),conn ,snapshot ,args ) 235 } 236res .Effort = & effort 237case "/raise-cap" : 238enabled := true 239switch strings .TrimSpace (args ) { 240case "" ,"on" ,"true" : 241case "off" ,"false" : 242enabled = false 243default : 244err = errors .New ("use /raise-cap [on|off]" ) 245 } 246if err != nil { 247break 248 } 249models ,readErr := daemon .ListProfileModels (context .Background (),conn ,currentProfile ) 250if readErr != nil { 251err = readErr 252break 253 } 254capKey := "" 255for _ ,model := range models { 256if model .ID == snapshot .Model { 257capKey = model .CapKey 258break 259 } 260 } 261acknowledgedModelsETag ,err = daemon .SetModelContextCap (context .Background (),conn , daemon.ModelContextCapRequest {CapKey :capKey ,ETag :modelsETag ,Enabled :enabled }) 262res .Message = "Context cap preference saved." 263case "/reload" : 264target := strings .TrimSpace (args ) 265if target == "" { 266target = "both" 267 } 268var reloaded daemon.SessionReloadResult 269reloaded ,err = daemon .ReloadSession (context .Background (),conn ,sessionID , daemon.ReloadRequest {Target :target }) 270res .Message = reloaded .Message () 271case "/kernel" : 272var upgraded daemon.KernelUpgradeResult 273upgraded ,err = daemon .UpgradeKernel (context .Background (),conn ,sessionID ) 274res .Message = upgraded .Message () 275case "/compact" : 276var compacted daemon.CompactionResult 277compacted ,err = daemon .CompactSession (context .Background (),conn ,sessionID ,strings .TrimSpace (args )) 278res .Message = compacted .Message () 279default : 280// A name can carry a read and a change; typed text picks the one 281// that takes arguments. 282index := slices .IndexFunc (catalog ,func (item daemon.SessionCommand )bool { 283return item .Name == name && args != "" && len (item .Arguments )> 0 284 }) 285if index < 0 { 286index = slices .IndexFunc (catalog ,func (item daemon.SessionCommand )bool {return item .Name == name }) 287 } 288if index < 0 { 289err = fmt .Errorf ("command %s is not in the loaded catalog" ,name ) 290break 291 } 292command := catalog [index ] 293arguments ,parseErr := daemon .ParseCommandArguments (command ,args ) 294if parseErr != nil { 295err = parseErr 296break 297 } 298res ,err = daemon .InvokeDeclaredCommand (context .Background (),conn ,snapshot ,command ,arguments ) 299 } 300 301if err != nil { 302return commandExecutedMsg {Name :name ,Err :err ,Gen :gen } 303 } 304 305msg := fmt .Sprintf ("%s done" ,name ) 306var newEffort string 307if res .Effort != nil { 308if args == "" { 309return commandExecutedMsg {Name :name ,Available :res .Effort .Available ,SessionID :sessionID ,Gen :gen } 310 } 311newEffort = res .Effort .Effort 312if res .Effort .Message != "" { 313msg = res .Effort .Message 314 } 315 }else if res .Message != "" { 316msg = res .Message 317 } 318 319return commandExecutedMsg {Name :name ,Message :msg ,Page :res .Page ,Effort :newEffort ,ModelsETag :acknowledgedModelsETag ,ETag :func ()string { 320if res .Effort != nil { 321return res .Effort .ETag 322 } 323return "" 324 }(),EffortChanged :res .Effort != nil ,SessionID :sessionID ,Gen :gen } 325 } 326} 327 328func profilesForSession (profiles config.Profiles ,session daemon.Session ) config.Settings { 329profile := profiles .Providers [session .Provider ] 330profile .ProfileName = session .Provider 331return profile 332} 333 334func (m * AppModel )handleCommandInvocation (msg tea.Msg ) (tea.Cmd ,bool ) { 335switch msg := msg .(type ) { 336case ChatExecuteCommandMsg : 337if slices .ContainsFunc (m .CommandCatalog ,func (command daemon.SessionCommand )bool { 338return command .Name == msg .Name && command .Delivery == "input" 339 }) { 340var commands []tea.Cmd 341m .Chat .handleSubmittedCommand (strings .TrimSpace (msg .Name + " " + msg .Args ),& commands ) 342return tea .Batch (commands ... ),true 343 } 344switch msg .Name { 345case "/login" : 346return m .openLogin (msg .Args ),true 347case "/model" : 348if msg .Args != "" { 349m .ModelGen ++ 350return m .changeModelCmd (msg .Args ,"" ,"" ,nil ,m .ModelGen ,"" ),true 351 } 352return m .openModelPicker (),true 353case "/work" ,"/paperclips" : 354if m .ActiveSession == nil { 355return nil ,true 356 } 357if strings .TrimSpace (msg .Args )== "" { 358return func () tea.Msg {return ChatOpenPageMsg {Command :msg .Name } },true 359 } 360m .PageView = NewPageViewModel (m .Conn ,m .ActiveSession .ID ,msg .Name ) 361m .PageView .SetSize (m .Width ,m .Height ) 362m .State = AppStatePageView 363return m .PageView .shortcutCmd (msg .Args ),true 364case "/link" : 365if m .ActiveSession == nil { 366return nil ,true 367 } 368action ,target ,_ := strings .Cut (strings .TrimSpace (msg .Args )," " ) 369if action == "add" && strings .TrimSpace (target )!= "" { 370snapshot := * m .ActiveSession 371conn := m .Conn 372m .PageView = NewPageViewModel (conn ,snapshot .ID ,"/links" ) 373m .PageView .SetSize (m .Width ,m .Height ) 374m .State = AppStatePageView 375gen := m .PageView .Generation 376return func () tea.Msg { 377doc ,err := daemon .PrepareLinkMerge (context .Background (),conn ,snapshot ,strings .TrimSpace (target )) 378return pageLoadedMsg {Doc :doc ,Err :err ,Gen :gen } 379 },true 380 } 381return func () tea.Msg {return ChatOpenPageMsg {Command :"/links" } },true 382case "/new" : 383return m .newSessionCmd (),true 384case "/agents" : 385return func () tea.Msg {return ChatOpenAgentsMsg {} },true 386case "/sessions" ,"/a" : 387return m .openSessions (),true 388case "/extensions" ,"/plugins" : 389return m .openExtensionPicker (),true 390case "/tree" : 391return m .openTreePicker (),true 392case "/quota" : 393return m .openQuota (),true 394case "/cd" : 395if msg .Args == "" || m .ActiveSession == nil { 396return m .openFolderPicker (nil ),true 397 } 398return moveCmd (daemonFolders {conn :m .Conn ,condition : daemon.SessionCondition {ETag :m .ActiveSession .ETag ,FamilyRevision :m .ActiveSession .FamilyRevision }},m .ActiveSession .ID ,m .ActiveSession .Workspace ,msg .Args ,nil ),true 399case "/context" : 400return m .openContextInspector (),true 401default : 402m .CommandGen ++ 403return m .executeCommandCmd (msg .Name ,msg .Args ,m .CommandGen ),true 404 } 405 } 406return nil ,false 407}