okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
bb7b485
main
1package tui 2 3import ( 4"albedo/cli/internal/daemon" 5"context" 6"errors" 7"fmt" 8"slices" 9"strings" 10"time" 11 12 tea"charm.land/bubbletea/v2" 13) 14 15// Moved takes the session's new folder, then sends again the turn a 16// missing folder refused. 17func (m * ChatModel )Moved (moved daemon.Session ,retry * WorkspaceRetry ) tea.Cmd { 18workspace ,from := moved .Workspace ,m .Workspace 19m .dropSignIn ()// the offer was for the old folder's host 20// a hint from an earlier move names a folder this one did not leave 21m .Notices = slices .DeleteFunc (m .Notices ,func (n Notice )bool {return strings .HasPrefix (n .Message ,linkHintText ) }) 22m .Notices .AddNotice (linkHint (from ,workspace ,retry )) 23m .syncViewportHeight () 24m .Workspace ,m .Host ,m .hostHome = workspace ,sessionHost (moved ),"" 25m .Renderer .Workspace = workspace 26m .rebuildSettledLines ()// settled rows name paths from the old workspace 27m .refreshViewportContent () 28return m .resend (retry ) 29} 30 31const linkHintText = "to share memory and work with the folder you left: /link add " 32 33// linkHint offers linking the folder a session just left with the one it 34// moved to, as a command to paste. The folder is named as stored, so the 35// link lands on the key the left folder's memory and work items use. A 36// session that moved because its folder went missing has nothing to link. 37func linkHint (from ,to string ,retry * WorkspaceRetry )string { 38if from == "" || from == to || retry != nil { 39return "" 40 } 41return linkHintText + from 42} 43 44// resend sends again a turn the daemon refused, once what refused it is fixed. 45func (m * ChatModel )resend (retry * WorkspaceRetry ) tea.Cmd { 46if retry == nil { 47return nil 48 } 49m .Images .restore (unseen (retry .Images )) 50m .Pastes .restore (retry .Pastes ) 51m .TextArea .Reset () 52m .syncLayout () 53var cmds []tea.Cmd 54prompt := retry .Prompt 55if retry .Continue { 56prompt = "." 57 } 58m .submitInput (prompt ,& cmds ) 59return tea .Batch (cmds ... ) 60} 61 62func (m ChatModel )queryOperationCmd (handle * daemon.OperationHandle ) tea.Cmd { 63client ,id ,gen ,ctx := m .client ,m .SessionID ,m .Generation ,m .streamCtx 64return func () tea.Msg { 65receipt ,err := client .ResolveOperation (ctx ,handle ) 66return ChatOperationResolvedMsg {SessionID :id ,Generation :gen ,Handle :handle ,Receipt :receipt ,Err :err } 67 } 68} 69 70func (m ChatModel )resolveOperationCmd (handle * daemon.OperationHandle ) tea.Cmd { 71id ,gen := m .SessionID ,m .Generation 72return tea .Tick (15 * time .Second ,func (time.Time ) tea.Msg {return ChatOperationPollMsg {SessionID :id ,Generation :gen ,Handle :handle } }) 73} 74 75func (m * ChatModel )sendCmd (handle * daemon.OperationHandle ,prompt string ,images []daemon.ImageAttachment ,pastes []string ,isCont bool ) tea.Cmd { 76client ,id ,gen := m .client ,m .SessionID ,m .Generation 77return func () tea.Msg { 78msg := ChatTurnSentMsg {SessionID :id ,Generation :gen ,Prompt :prompt ,Images :images ,Pastes :pastes ,Continue :isCont ,Handle :handle ,OperationID :handle .ID ()} 79result ,err := client .SubmitOperation (context .Background (),handle ) 80msg .Err = err 81if err == nil && result != nil { 82msg .OK ,msg .Queued ,msg .AcceptanceOrder = result .OK ,result .Queued ,result .AcceptanceOrder 83 } 84return msg 85 } 86} 87 88func (m * ChatModel )interruptCmd () tea.Cmd { 89client ,id ,gen := m .client ,m .SessionID ,m .Generation 90status := m .Status 91return func () tea.Msg { 92ok ,err := client .InterruptObserved (context .Background (),status ) 93return ChatInterruptMsg {SessionID :id ,Generation :gen ,Interrupted :ok ,Err :err } 94 } 95} 96 97func (m ChatModel )pendingSendCount ()int { 98count := 0 99for _ ,pending := range m .pendingUsers { 100if ! pending .Expired { 101count ++ 102 } 103 } 104return count 105} 106 107func (m ChatModel )operationRecoverable (id string )bool { 108for _ ,pending := range m .pendingUsers { 109if pending .OperationID == id { 110return ! pending .Expired 111 } 112 } 113pending ,exists := m .pendingContinuations [id ] 114return exists && ! pending .Expired 115} 116 117func (m * ChatModel )reconcileStopped () { 118if m .Stopping && ! m .isSending && ! m .interruptDeferred && m .pendingSendCount ()== 0 && (! m .Status .Running || m .Status .Idle ) { 119m .Stopping = false 120m .Stopped = true 121m .clearAction () 122 } 123} 124 125func (m * ChatModel )applyInputReceipt (receipt daemon.InputReceipt ) { 126m .Status .InputOrder = max (m .Status .InputOrder ,receipt .InputOrder ()) 127for i := range m .pendingUsers { 128if m .pendingUsers [i ].OperationID == receipt .ID { 129m .pendingUsers [i ].Queued = receipt .Pending () 130if receipt .Settled () { 131m .pendingUsers = slices .Delete (m .pendingUsers ,i ,i + 1 ) 132 } 133m .reconcileStopped () 134return 135 } 136 } 137if receipt .Settled () { 138delete (m .pendingContinuations ,receipt .ID ) 139 } 140m .reconcileStopped () 141} 142 143func (m * ChatModel )handleOperationResults (msg tea.Msg ) (tea.Cmd ,bool ) { 144var cmds []tea.Cmd 145switch msg := msg .(type ) { 146case ChatOperationPollMsg : 147if msg .SessionID != m .SessionID || msg .Generation != m .Generation || ! m .operationRecoverable (msg .Handle .ID ()) { 148return nil ,true 149 } 150return m .queryOperationCmd (msg .Handle ),true 151case ChatOperationResolvedMsg : 152if msg .SessionID != m .SessionID || msg .Generation != m .Generation { 153return nil ,true 154 } 155index := slices .IndexFunc (m .pendingUsers ,func (p PendingUserTurn )bool {return p .OperationID == msg .Handle .ID () }) 156if ! m .operationRecoverable (msg .Handle .ID ()) { 157return nil ,true 158 } 159if daemon .IsOperationExpired (msg .Err ) { 160if index >= 0 { 161m .pendingUsers [index ].Expired = true 162 }else { 163pending := m .pendingContinuations [msg .Handle .ID ()] 164pending .Expired = true 165m .pendingContinuations [msg .Handle .ID ()]= pending 166 } 167m .refreshViewportContent () 168return nil ,true 169 } 170if msg .Err == nil && (msg .Receipt .Admission == "rejected" || msg .Receipt .Settled ()) { 171delete (m .pendingContinuations ,msg .Handle .ID ()) 172m .dropSignIn () 173if rejection := msg .Receipt .Rejection ();rejection != nil { 174m .ClearNotices () 175m .AddError (rejection .Error ()) 176 } 177if index >= 0 { 178m .pendingUsers = slices .Delete (m .pendingUsers ,index ,index + 1 ) 179 } 180m .refreshViewportContent () 181m .reconcileStopped () 182return nil ,true 183 } 184if msg .Err == nil { 185m .Status .InputOrder = max (m .Status .InputOrder ,msg .Receipt .InputOrder ()) 186 } 187if index >= 0 && msg .Err == nil { 188m .pendingUsers [index ].Queued = true 189m .pendingUsers [index ].BlockingReason = msg .Receipt .BlockingDetail () 190m .refreshViewportContent () 191 } 192return tea .Batch (m .resolveOperationCmd (msg .Handle ),m .hostAuthCmd (msg .Receipt .BlockingDetail ())),true 193case ChatTurnSentMsg : 194if msg .SessionID != m .SessionID || msg .Generation != m .Generation { 195return nil ,true 196 } 197m .isSending = false 198if msg .Err == nil { 199m .Status .InputOrder = max (m .Status .InputOrder ,msg .AcceptanceOrder ) 200 } 201if msg .Err != nil { 202m .interruptDeferred = false 203if _ ,uncertain := errors.AsType [* daemon.UncertainOutcomeError ](msg .Err );uncertain { 204if msg .Handle != nil && m .operationRecoverable (msg .Handle .ID ())&& daemon .IsOperationExpired (msg .Err ) { 205updated ,cmd := m .Update (ChatOperationResolvedMsg {SessionID :m .SessionID ,Generation :m .Generation ,Handle :msg .Handle ,Err :msg .Err }) 206* m = updated 207return cmd ,true 208 } 209message := "Message admission is uncertain. Checking its operation receipt." 210if msg .Continue { 211message = "Continue admission is uncertain. Checking its operation receipt." 212 } 213m .AddError (message ) 214m .refreshViewportContent () 215if msg .Handle != nil && m .operationRecoverable (msg .Handle .ID ()) { 216return m .resolveOperationCmd (msg .Handle ),true 217 } 218return nil ,true 219 } 220delete (m .pendingContinuations ,msg .OperationID ) 221if ! msg .Continue { 222if i := slices .IndexFunc (m .pendingUsers ,func (p PendingUserTurn )bool {return p .OperationID == msg .OperationID });i >= 0 { 223m .pendingUsers = slices .Delete (m .pendingUsers ,i ,i + 1 ) 224 } 225if m .TextArea .Value ()== "" { 226m .TextArea .SetValue (msg .Prompt ) 227m .Images .restore (unseen (msg .Images )) 228m .Pastes .restore (msg .Pastes ) 229 } 230 } 231if apiErr ,ok := errors.AsType [* daemon.APIError ](msg .Err );ok && (apiErr .Code == "workspace_invalid" || apiErr .Code == "workspace_unavailable" )&& m .Workspace != "" { 232m .Status .Running ,m .Status .Idle = false ,true 233retry := & WorkspaceRetry {Missing :m .Workspace ,Prompt :msg .Prompt ,Continue :msg .Continue ,Images :msg .Images ,Pastes :msg .Pastes } 234cmds = append (cmds ,func () tea.Msg {return ChatOpenFolderPickerMsg {Retry :retry } }) 235 }else { 236m .AddError (fmt .Sprintf ("Could not send the message: %v" ,msg .Err )) 237errText := msg .Err .Error () 238if msg .Queued { 239errText = "Message was not queued: " + errText 240 } 241m .appendSettledEntry (HistoryEntry {Kind :EntryError ,Text :errText }) 242 } 243m .refreshViewportContent () 244return tea .Batch (cmds ... ),true 245 } 246 247if msg .Queued { 248if i := slices .IndexFunc (m .pendingUsers ,func (p PendingUserTurn )bool { 249return p .OperationID == msg .OperationID && ! p .Queued 250 });i >= 0 { 251m .pendingUsers [i ].Queued = true 252 } 253 } 254if msg .Handle != nil && (msg .Continue || slices .ContainsFunc (m .pendingUsers ,func (p PendingUserTurn )bool {return p .OperationID == msg .OperationID })) { 255cmds = append (cmds ,m .resolveOperationCmd (msg .Handle )) 256 } 257 258// If user pressed Esc while this send was in flight, dispatch interrupt now that send is accepted 259if m .interruptDeferred { 260m .interruptDeferred = false 261m .Stopping = true 262cmds = append (cmds ,m .interruptCmd ()) 263 }else { 264m .reconcileStopped () 265 } 266return tea .Batch (cmds ... ),true 267case ChatInterruptMsg : 268if msg .SessionID != m .SessionID || msg .Generation != m .Generation { 269return nil ,true 270 } 271if msg .Err != nil || ! msg .Interrupted { 272m .Stopping = false 273if msg .Err != nil { 274m .appendSettledEntry (HistoryEntry {Kind :EntryError ,Text :"Could not stop the current reply: " + msg .Err .Error ()}) 275 } 276 } 277m .reconcileStopped () 278return nil ,true 279 } 280return nil ,false 281}