package app import ( "context" "time" ) const operationTimeout = 5 * time.Minute // StatusKind gives presentation layers semantics without requiring them to // inspect user-facing status text. type StatusKind uint8 const ( StatusProgress StatusKind = iota + 1 StatusSuccess StatusError StatusBlocked ) // OperationStatus is concise status text related to an optional Target. type OperationStatus struct { Kind StatusKind Summary string TargetID string } // Confirmation is a structured, safe-default decision. Presentation layers // render these fields but never infer confirmation state from their wording. type Confirmation struct { Title string Summary string Details []string ConfirmLabel string CancelLabel string HighImpact bool // Token binds acceptance to the exact state and operation scope that was // presented. Presentation layers must return it with the confirmed Intent. Token string } // TargetProgressPhase describes one Target's part in an operation stream. type TargetProgressPhase uint8 const ( TargetProgressRunning TargetProgressPhase = iota + 1 TargetProgressSucceeded TargetProgressFailed ) // OperationState is a closed tagged set. Its concrete value makes an update // exactly one of running, confirmation, succeeded, failed, or closed. type OperationState interface { operationState() decode() DecodedOperationUpdate } // OperationUpdateKind is the decoded variant of one operation stream update. // Consumers decode once and handle the payload without repeating type switches. type OperationUpdateKind uint8 const ( OperationRunning OperationUpdateKind = iota + 1 OperationConfirmation OperationSucceeded OperationFailed OperationClosed ) // DecodedOperationUpdate is the complete behavior-bearing projection of an // OperationUpdate variant. Fields that do not apply to Kind retain zero values. type DecodedOperationUpdate struct { Kind OperationUpdateKind Status OperationStatus Progress []TargetProgress Confirmation *Confirmation Err error FirstFailedTargetID string } func (u DecodedOperationUpdate) IsTerminal() bool { return u.Kind != OperationRunning } type operationRunning struct { status OperationStatus progress []TargetProgress } func (operationRunning) operationState() {} func (state operationRunning) decode() DecodedOperationUpdate { return DecodedOperationUpdate{Kind: OperationRunning, Status: state.status, Progress: cloneProgress(state.progress)} } type operationConfirmation struct{ request Confirmation } func (operationConfirmation) operationState() {} func (state operationConfirmation) decode() DecodedOperationUpdate { request := state.request request.Details = append([]string(nil), request.Details...) return DecodedOperationUpdate{Kind: OperationConfirmation, Confirmation: &request} } type operationSucceeded struct { status OperationStatus progress []TargetProgress } func (operationSucceeded) operationState() {} func (state operationSucceeded) decode() DecodedOperationUpdate { return DecodedOperationUpdate{Kind: OperationSucceeded, Status: state.status, Progress: cloneProgress(state.progress)} } type operationFailed struct { status OperationStatus progress []TargetProgress firstFailedTargetID string err error } func (operationFailed) operationState() {} func (state operationFailed) decode() DecodedOperationUpdate { return DecodedOperationUpdate{ Kind: OperationFailed, Status: state.status, Progress: cloneProgress(state.progress), Err: state.err, FirstFailedTargetID: state.firstFailedTargetID, } } type operationClosed struct{ status OperationStatus } func (operationClosed) operationState() {} func (state operationClosed) decode() DecodedOperationUpdate { return DecodedOperationUpdate{Kind: OperationClosed, Status: state.status} } // OperationUpdate is the common progressive contract used by interactive // detection, Target mutation, and Active Profile transition operations. Build // values with the constructors below so contradictory states are unrepresentable. type OperationUpdate struct { Snapshot Snapshot State OperationState } func RunningOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate { status.Kind = StatusProgress return OperationUpdate{Snapshot: snapshot, State: operationRunning{status: status, progress: cloneProgress(progress)}} } func ConfirmationOperationUpdate(snapshot Snapshot, request Confirmation) OperationUpdate { return OperationUpdate{Snapshot: snapshot, State: operationConfirmation{request: request}} } func SucceededOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress) OperationUpdate { status.Kind = StatusSuccess return OperationUpdate{Snapshot: snapshot, State: operationSucceeded{status: status, progress: cloneProgress(progress)}} } func FailedOperationUpdate(snapshot Snapshot, status OperationStatus, progress []TargetProgress, firstFailedTargetID string, err error) OperationUpdate { status.Kind = StatusError return OperationUpdate{Snapshot: snapshot, State: operationFailed{ status: status, progress: cloneProgress(progress), firstFailedTargetID: firstFailedTargetID, err: err, }} } // ClosedOperationUpdate reports a request that cannot start, such as an // overlapping interactive operation. It is terminal and has no progress or // error payload. func ClosedOperationUpdate(snapshot Snapshot, status OperationStatus) OperationUpdate { status.Kind = StatusBlocked return OperationUpdate{Snapshot: snapshot, State: operationClosed{status: status}} } func cloneProgress(progress []TargetProgress) []TargetProgress { return append([]TargetProgress(nil), progress...) } // Decode centralizes the closed-set state decoding for all consumers. func (u OperationUpdate) Decode() DecodedOperationUpdate { if u.State == nil { return DecodedOperationUpdate{} } return u.State.decode() } func (u OperationUpdate) Status() OperationStatus { return u.Decode().Status } func (u OperationUpdate) Progress() []TargetProgress { return u.Decode().Progress } func (u OperationUpdate) Confirmation() *Confirmation { return u.Decode().Confirmation } func (u OperationUpdate) Err() error { return u.Decode().Err } func (u OperationUpdate) FirstFailedTargetID() string { return u.Decode().FirstFailedTargetID } func (u OperationUpdate) IsRunning() bool { return u.Decode().Kind == OperationRunning } func (u OperationUpdate) IsConfirmation() bool { return u.Decode().Kind == OperationConfirmation } func (u OperationUpdate) IsSucceeded() bool { return u.Decode().Kind == OperationSucceeded } func (u OperationUpdate) IsFailed() bool { return u.Decode().Kind == OperationFailed } func (u OperationUpdate) IsTerminal() bool { return u.Decode().IsTerminal() } // OperationStream exposes the useful initial frame synchronously and all // subsequent structured changes through Updates. Updates closes after a // terminal update. type OperationStream struct { Snapshot Snapshot Updates <-chan OperationUpdate Cancel context.CancelFunc } // InteractiveModule is the presentation-independent Application seam used by // the TUI. Module.Dispatch remains available for synchronous callers. type InteractiveModule interface { Module StartOperation(Intent) OperationStream } // StartOperation returns before dispatch serialization is acquired. This keeps // Bubble Tea Update non-blocking even when a synchronous caller currently owns // the serialized Application entry. func (a *Application) StartOperation(intent Intent) OperationStream { initial := a.Snapshot() a.interactiveMu.Lock() if a.interactiveOperation { a.interactiveMu.Unlock() updates := make(chan OperationUpdate, 1) updates <- ClosedOperationUpdate(initial, OperationStatus{ Summary: "Another system operation is already in progress", }) close(updates) return OperationStream{Snapshot: initial, Updates: updates} } a.interactiveOperation = true a.interactiveMu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) updates := make(chan OperationUpdate, 2*len(a.targets)+2) go func() { defer close(updates) defer cancel() final := a.dispatch(ctx, intent, func(result Result) { updates <- runningOperationUpdate(result) }) // Release the Application gate before publishing the terminal update so // an immediate confirmation retry cannot observe a stale busy state. a.finishInteractiveOperation() updates <- terminalOperationUpdate(final) }() return OperationStream{Snapshot: initial, Updates: updates, Cancel: cancel} } func (a *Application) finishInteractiveOperation() { a.interactiveMu.Lock() a.interactiveOperation = false a.interactiveMu.Unlock() } func runningOperationUpdate(result Result) OperationUpdate { if result.ConfirmationRequest != nil { return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest) } return RunningOperationUpdate(result.Snapshot, result.Status, result.Progress) } func terminalOperationUpdate(result Result) OperationUpdate { if result.ConfirmationRequest != nil { return ConfirmationOperationUpdate(result.Snapshot, *result.ConfirmationRequest) } status := result.Status if result.Err != nil { if status.Summary == "" { status.Summary = "Operation failed" } return FailedOperationUpdate(result.Snapshot, status, result.Progress, result.FirstFailedTargetID, result.Err) } if status.Summary == "" { status.Summary = result.Notice } return SucceededOperationUpdate(result.Snapshot, status, result.Progress) }