package tui import ( "errors" tea "github.com/charmbracelet/bubbletea" "proxyctl/internal/app" ) type operationError struct { status app.OperationStatus detail string } // operationCoordinator owns the complete interactive operation lifecycle. The // root Model delegates stream consumption, progress, confirmation, current // error, and deferred-exit state here instead of mirroring those states. type operationCoordinator struct { active bool intent app.Intent updates <-chan app.OperationUpdate progress map[string]app.TargetProgressPhase status app.OperationStatus currentError *operationError errorExpanded bool errorOffset int confirmation *app.Confirmation confirmationDialog confirmationDialog confirmationIntent *app.Intent quitRequested bool } type operationProgress struct { update app.OperationUpdate updates <-chan app.OperationUpdate closed bool } func newOperationCoordinator() operationCoordinator { return operationCoordinator{progress: make(map[string]app.TargetProgressPhase)} } func waitForOperation(updates <-chan app.OperationUpdate) tea.Cmd { return func() tea.Msg { update, ok := <-updates if !ok { return operationProgress{updates: updates, closed: true} } return operationProgress{update: update, updates: updates} } } func (o *operationCoordinator) start(application app.InteractiveModule, intent app.Intent) (app.Snapshot, tea.Cmd, bool) { if o.quitRequested || o.active || o.confirmation != nil { o.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current operation to finish before starting another modification"} return application.Snapshot(), nil, false } stream := application.StartOperation(intent) o.active = true o.intent = intent o.updates = stream.Updates o.progress = make(map[string]app.TargetProgressPhase) if intent.TargetID != "" { o.progress[intent.TargetID] = app.TargetProgressRunning } o.status = app.OperationStatus{Kind: app.StatusProgress, Summary: operationStartingSummary(intent.Kind)} return stream.Snapshot, waitForOperation(stream.Updates), true } func operationStartingSummary(kind app.IntentKind) string { switch kind { case app.RefreshTargets: return "Checking Target availability and state…" case app.ActivateProfile: return "Changing Active Profile…" default: return "Starting Target mutation…" } } func (o *operationCoordinator) apply(update app.OperationUpdate) (firstFailedTargetID string) { status := update.Status() progress := update.Progress() for _, item := range progress { o.progress[item.TargetID] = item.Phase if item.Phase == app.TargetProgressSucceeded { o.resolveTargetError(item.TargetID) } } if status.Summary != "" { o.status = status } if update.IsSucceeded() { o.resolveTargetError(status.TargetID) } if update.IsFailed() { o.currentError = &operationError{status: status, detail: errorDetail(update.Err())} o.errorExpanded = false o.errorOffset = 0 firstFailedTargetID = update.FirstFailedTargetID() } if request := update.Confirmation(); request != nil { intent := o.intent o.confirmation = request o.confirmationDialog = newConfirmationDialog(*request) o.confirmationIntent = &intent } if update.IsTerminal() { o.finish() } return firstFailedTargetID } func errorDetail(err error) string { if err == nil { return "" } return err.Error() } func (o *operationCoordinator) resolveTargetError(targetID string) { if targetID == "" || o.currentError == nil || o.currentError.status.TargetID != targetID { return } o.currentError = nil o.errorExpanded = false o.errorOffset = 0 } func (o *operationCoordinator) finish() { o.active = false o.intent = app.Intent{} o.updates = nil } func (o *operationCoordinator) unexpectedClose() { o.status = app.OperationStatus{Kind: app.StatusError, Summary: "Operation ended without a final result"} o.currentError = &operationError{ status: o.status, detail: "The Application operation stream closed before publishing a terminal result.", } o.errorExpanded = false o.errorOffset = 0 o.finish() } func (o *operationCoordinator) acknowledgeError() { o.currentError = nil o.errorExpanded = false o.errorOffset = 0 if o.status.Kind == app.StatusError { o.status = app.OperationStatus{} } } func (o *operationCoordinator) cancelConfirmation() { o.confirmation = nil o.confirmationDialog = confirmationDialog{} o.confirmationIntent = nil o.status = app.OperationStatus{} } func (o *operationCoordinator) confirmedIntent() (app.Intent, bool) { if o.confirmationIntent == nil { return app.Intent{}, false } intent := *o.confirmationIntent intent.Confirm = true o.confirmation = nil o.confirmationDialog = confirmationDialog{} o.confirmationIntent = nil return intent, true } func (o operationCoordinator) busy() bool { return o.quitRequested || o.active || o.confirmation != nil } func (o operationCoordinator) deferredQuit() tea.Cmd { if o.quitRequested && !o.active { return tea.Quit } return nil } func (o *operationCoordinator) requestQuit() tea.Cmd { o.quitRequested = true if o.active { return nil } return tea.Quit } func (o *operationCoordinator) fail(err error, summary string) { if err == nil { err = errors.New(summary) } o.status = app.OperationStatus{Kind: app.StatusError, Summary: summary} o.currentError = &operationError{status: o.status, detail: err.Error()} o.finish() }