okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e2130b
main
1package tui 2 3import ( 4"albedo/cli/internal/daemon" 5"errors" 6"time" 7 8 tea"charm.land/bubbletea/v2" 9) 10 11func (m ChatModel )animating ()bool { 12return m .isSending || m .pendingSendCount ()> 0 || m .Stopping || m .latestProgress ()!= nil || m .Status .Running && ! m .Status .Idle || m .reaching ()!= "" || m .backgroundJobCount ()> 0 13} 14 15func (m * ChatModel )startAnimation () tea.Cmd { 16if m .streamStopped || m .animationActive || ! m .animating () { 17return nil 18 } 19m .animationActive = true 20return m .progressTickCmd () 21} 22 23func (m ChatModel )progressTickCmd () tea.Cmd { 24id ,generation := m .SessionID ,m .Generation 25return tea .Tick (faceInterval ,func (time.Time ) tea.Msg {return ChatProgressTickMsg {SessionID :id ,Generation :generation } }) 26} 27 28// onlyGrew is whether an event did nothing but lengthen the reply streaming 29// in, given the stream state and history length from before it. 30func (m ChatModel )onlyGrew (before transcriptState ,entries int )bool { 31after := m .transcript 32return before .activeText != "" && len (after .activeText )> len (before .activeText )&& 33after .activeKind == before .activeKind && after .activeMessageID == before .activeMessageID && 34m .History .Len ()== entries 35} 36 37// drawLiveLater redraws the growing reply once its interval has passed. 38func (m * ChatModel )drawLiveLater () tea.Cmd { 39if m .liveDue { 40return nil 41 } 42m .liveDue = true 43id ,generation := m .SessionID ,m .Generation 44return tea .Tick (time .Until (m .liveDrawn .Add (m .liveInterval )),func (time.Time ) tea.Msg { 45return ChatLiveDrawMsg {SessionID :id ,Generation :generation } 46 }) 47} 48 49func (m ChatModel )waitForNextEvent () tea.Cmd { 50sessID ,gen ,ch ,ctx := m .SessionID ,m .Generation ,m .eventChan ,m .streamCtx 51return func () tea.Msg { 52select { 53case evt ,ok := <- ch : 54if ! ok { 55return ChatStreamClosedMsg {SessionID :sessID ,Generation :gen } 56 } 57if evt .Event != nil { 58return ChatStreamEventMsg {SessionID :sessID ,Generation :gen ,Event :* evt .Event } 59 } 60return ChatStreamResultMsg {SessionID :sessID ,Generation :gen ,Err :evt .Err ,Recovering :evt .Recovering } 61case <- ctx .Done (): 62return ChatStreamClosedMsg {SessionID :sessID ,Generation :gen } 63 } 64 } 65} 66 67func (m ChatModel )startStreamSubscription () tea.Cmd { 68client ,ctx ,ch := m .client ,m .streamCtx ,m .eventChan 69go func () { 70defer close (ch ) 71defer client .ResetStream () 72deliver := func (value streamDelivery )bool { 73select { 74case ch <- value : 75return true 76case <- ctx .Done (): 77return false 78 } 79 } 80recovered := false 81delay := 500 * time .Millisecond 82for ctx .Err ()== nil { 83err := client .StreamWithProgress (ctx ,olderPageRows ,func (evt daemon.StreamEvent )error { 84if ! deliver (streamDelivery {Event :& evt }) { 85return ctx .Err () 86 } 87return nil 88 },func () {delay = 500 * time .Millisecond }) 89if ctx .Err ()!= nil { 90return 91 } 92if failure ,ok := errors.AsType [* daemon.StreamError ](err );ok { 93switch failure .Kind { 94case daemon .StreamProtocol : 95if recovered { 96deliver (streamDelivery {Err :err }) 97return 98 } 99recovered = true 100if ! deliver (streamDelivery {Err :err ,Recovering :true }) { 101return 102 } 103client .ResetStream () 104case daemon .StreamTerminal : 105deliver (streamDelivery {Err :err }) 106return 107 } 108 } 109timer := time .NewTimer (delay ) 110select { 111case <- ctx .Done (): 112timer .Stop () 113return 114case <- timer .C : 115 } 116delay = min (delay * 2 ,5 * time .Second ) 117 } 118 }() 119return m .waitForNextEvent () 120} 121 122func (m ChatModel )clearCopyStatusCmd () tea.Cmd { 123id ,generation ,revision := m .SessionID ,m .Generation ,m .copyStatusRevision 124return tea .Tick (3 * time .Second ,func (time.Time ) tea.Msg { 125return ChatClearCopyStatusMsg {SessionID :id ,Generation :generation ,Revision :revision } 126 }) 127} 128 129func (m ChatModel )statusCmd () tea.Cmd { 130client ,ctx ,id ,generation ,revision := m .client ,m .streamCtx ,m .SessionID ,m .Generation ,m .statusRevision 131return func () tea.Msg { 132captured ,err := client .GetSession (ctx ) 133return ChatStatusMsg {SessionID :id ,Generation :generation ,Revision :revision ,Status :& captured .Status ,Glances :captured .Glances ,Err :err } 134 } 135} 136 137// windowCmd reads the context window when usage names a model whose window 138// has not been read yet. Only a usage event asks, so a failed read is tried 139// again at the next one rather than on every event. 140func (m ChatModel )windowCmd () tea.Cmd { 141if m .Usage == nil || m .windowModel != nil && * m .windowModel == m .Usage .Model { 142return nil 143 } 144client ,ctx ,id ,generation ,model := m .client ,m .streamCtx ,m .SessionID ,m .Generation ,m .Usage .Model 145return func () tea.Msg { 146tokens ,err := client .ContextWindow (ctx ) 147return ChatWindowMsg {SessionID :id ,Generation :generation ,Model :model ,Tokens :tokens ,Err :err } 148 } 149} 150 151// cacheFadeCmd wakes the footer when the cached count reaches its next step. 152func (m ChatModel )cacheFadeCmd () tea.Cmd { 153if m .Usage == nil { 154return nil 155 } 156now := time .Now ().UnixMilli () 157for _ ,step := range m .Usage .CacheFade { 158if step .At > now { 159id ,generation ,usage := m .SessionID ,m .Generation ,m .Usage 160return tea .Tick (time .Duration (step .At - now )* time .Millisecond ,func (time.Time ) tea.Msg { 161return ChatCacheFadeMsg {SessionID :id ,Generation :generation ,Usage :usage } 162 }) 163 } 164 } 165return nil 166} 167 168// hostHomeCmd asks a remote workspace's host where its home is, once per 169// workspace, after the kernel attached: the probe has answered by then. 170func (m * ChatModel )hostHomeCmd () tea.Cmd { 171host ,_ := daemon .SplitLocation (m .Workspace ) 172if host == "" || m .hostAsked == m .Workspace || m .Status .KernelLink != "attached" { 173return nil 174 } 175m .hostAsked = m .Workspace 176client ,ctx ,id ,generation ,workspace := m .client ,m .streamCtx ,m .SessionID ,m .Generation ,m .Workspace 177return func () tea.Msg { 178status ,err := client .Host (ctx ,host ) 179return ChatHostMsg {SessionID :id ,Generation :generation ,Workspace :workspace ,Status :status ,Err :err } 180 } 181} 182 183// statusPollCmd schedules the next status read: often while anything is live, 184// rarely while the session rests. Only the newest scheduled poll fires, so a 185// status read started elsewhere never adds a second polling loop. 186func (m * ChatModel )statusPollCmd () tea.Cmd { 187m .statusPoll ++ 188m .statusPollResting = ! m .animating ()&& m .Status .KernelLink != "booting" && m .Status .KernelLink != "reattaching" 189every := 750 * time .Millisecond 190if m .statusPollResting { 191every = 5 * time .Second 192 } 193id ,generation ,poll := m .SessionID ,m .Generation ,m .statusPoll 194return tea .Tick (every ,func (time.Time ) tea.Msg {return ChatStatusPollMsg {SessionID :id ,Generation :generation ,Poll :poll } }) 195} 196 197func (m ChatModel )Init () tea.Cmd { 198commands := []tea.Cmd {m .startStreamSubscription (),m .statusCmd ()} 199for _ ,pending := range m .pendingUsers { 200if pending .Handle != nil && ! pending .Expired { 201commands = append (commands ,m .queryOperationCmd (pending .Handle )) 202 } 203 } 204for _ ,pending := range m .pendingContinuations { 205if ! pending .Expired { 206commands = append (commands ,m .queryOperationCmd (pending .Handle )) 207 } 208 } 209return tea .Batch (commands ... ) 210}