pi-agent: Fix final integrated review
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"proxyctl/internal/app"
|
||||
@@ -11,11 +12,12 @@ import (
|
||||
type operationError struct {
|
||||
status app.OperationStatus
|
||||
detail string
|
||||
intent app.Intent
|
||||
}
|
||||
|
||||
// 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.
|
||||
// error, spinner, and deferred-exit state here instead of mirroring them.
|
||||
type operationCoordinator struct {
|
||||
active bool
|
||||
intent app.Intent
|
||||
@@ -29,6 +31,7 @@ type operationCoordinator struct {
|
||||
confirmationDialog confirmationDialog
|
||||
confirmationIntent *app.Intent
|
||||
quitRequested bool
|
||||
spinner spinner.Model
|
||||
}
|
||||
|
||||
type operationProgress struct {
|
||||
@@ -37,20 +40,50 @@ type operationProgress struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newOperationCoordinator() operationCoordinator {
|
||||
return operationCoordinator{progress: make(map[string]app.TargetProgressPhase)}
|
||||
var applyingSpinner = spinner.Spinner{
|
||||
Frames: []string{"|", "/", "-", "\\"},
|
||||
FPS: 90 * time.Millisecond,
|
||||
}
|
||||
|
||||
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 newOperationCoordinator() operationCoordinator {
|
||||
return operationCoordinator{
|
||||
progress: make(map[string]app.TargetProgressPhase),
|
||||
spinner: spinner.New(spinner.WithSpinner(applyingSpinner)),
|
||||
}
|
||||
}
|
||||
|
||||
// waitForOperation races the stream against the real Bubbles tick command.
|
||||
// Only the stream is read directly, so a losing tick cannot consume an update.
|
||||
func waitForOperation(updates <-chan app.OperationUpdate, tick tea.Cmd) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if tick == nil {
|
||||
update, ok := <-updates
|
||||
if !ok {
|
||||
return operationProgress{updates: updates, closed: true}
|
||||
}
|
||||
return operationProgress{update: update, updates: updates}
|
||||
}
|
||||
ticks := make(chan tea.Msg, 1)
|
||||
go func() { ticks <- tick() }()
|
||||
select {
|
||||
case update, ok := <-updates:
|
||||
if !ok {
|
||||
return operationProgress{updates: updates, closed: true}
|
||||
}
|
||||
return operationProgress{update: update, updates: updates}
|
||||
case message := <-ticks:
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o operationCoordinator) wait() tea.Cmd {
|
||||
if !o.active || o.updates == nil {
|
||||
return nil
|
||||
}
|
||||
return waitForOperation(o.updates, o.spinner.Tick)
|
||||
}
|
||||
|
||||
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"}
|
||||
@@ -61,11 +94,12 @@ func (o *operationCoordinator) start(application app.InteractiveModule, intent a
|
||||
o.intent = intent
|
||||
o.updates = stream.Updates
|
||||
o.progress = make(map[string]app.TargetProgressPhase)
|
||||
o.spinner = spinner.New(spinner.WithSpinner(applyingSpinner))
|
||||
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
|
||||
return stream.Snapshot, o.wait(), true
|
||||
}
|
||||
|
||||
func operationStartingSummary(kind app.IntentKind) string {
|
||||
@@ -79,34 +113,38 @@ func operationStartingSummary(kind app.IntentKind) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (o *operationCoordinator) tick(message spinner.TickMsg) tea.Cmd {
|
||||
if !o.active {
|
||||
return nil
|
||||
}
|
||||
updated, nextTick := o.spinner.Update(message)
|
||||
o.spinner = updated
|
||||
return waitForOperation(o.updates, nextTick)
|
||||
}
|
||||
|
||||
func (o *operationCoordinator) apply(update app.OperationUpdate) (firstFailedTargetID string) {
|
||||
status := update.Status()
|
||||
progress := update.Progress()
|
||||
for _, item := range progress {
|
||||
decoded := update.Decode()
|
||||
for _, item := range decoded.Progress {
|
||||
o.progress[item.TargetID] = item.Phase
|
||||
if item.Phase == app.TargetProgressSucceeded {
|
||||
o.resolveTargetError(item.TargetID)
|
||||
}
|
||||
}
|
||||
if status.Summary != "" {
|
||||
o.status = status
|
||||
if decoded.Status.Summary != "" {
|
||||
o.status = decoded.Status
|
||||
}
|
||||
if update.IsSucceeded() {
|
||||
o.resolveTargetError(status.TargetID)
|
||||
}
|
||||
if update.IsFailed() {
|
||||
o.currentError = &operationError{status: status, detail: errorDetail(update.Err())}
|
||||
switch decoded.Kind {
|
||||
case app.OperationSucceeded:
|
||||
o.resolveError(o.intent)
|
||||
case app.OperationFailed:
|
||||
o.currentError = &operationError{status: decoded.Status, detail: errorDetail(decoded.Err), intent: o.intent}
|
||||
o.errorExpanded = false
|
||||
o.errorOffset = 0
|
||||
firstFailedTargetID = update.FirstFailedTargetID()
|
||||
}
|
||||
if request := update.Confirmation(); request != nil {
|
||||
firstFailedTargetID = decoded.FirstFailedTargetID
|
||||
case app.OperationConfirmation:
|
||||
intent := o.intent
|
||||
o.confirmation = request
|
||||
o.confirmationDialog = newConfirmationDialog(*request)
|
||||
o.confirmation = decoded.Confirmation
|
||||
o.confirmationDialog = newConfirmationDialog(*decoded.Confirmation)
|
||||
o.confirmationIntent = &intent
|
||||
}
|
||||
if update.IsTerminal() {
|
||||
if decoded.IsTerminal() {
|
||||
o.finish()
|
||||
}
|
||||
return firstFailedTargetID
|
||||
@@ -119,8 +157,8 @@ func errorDetail(err error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func (o *operationCoordinator) resolveTargetError(targetID string) {
|
||||
if targetID == "" || o.currentError == nil || o.currentError.status.TargetID != targetID {
|
||||
func (o *operationCoordinator) resolveError(success app.Intent) {
|
||||
if o.currentError == nil || !relatedResolution(o.currentError.intent, success) {
|
||||
return
|
||||
}
|
||||
o.currentError = nil
|
||||
@@ -128,6 +166,28 @@ func (o *operationCoordinator) resolveTargetError(targetID string) {
|
||||
o.errorOffset = 0
|
||||
}
|
||||
|
||||
func relatedResolution(failed, success app.Intent) bool {
|
||||
switch {
|
||||
case failed.Kind == app.RefreshTargets:
|
||||
return success.Kind == app.RefreshTargets
|
||||
case failed.Kind == app.ActivateProfile:
|
||||
return success.Kind == app.ActivateProfile && failed.Name == success.Name
|
||||
case isTargetMutation(failed.Kind):
|
||||
return isTargetMutation(success.Kind) && failed.TargetID != "" && failed.TargetID == success.TargetID
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isTargetMutation(kind app.IntentKind) bool {
|
||||
switch kind {
|
||||
case app.EnableTarget, app.DisableTarget, app.ApplyProfileToTarget, app.ReapplyActiveProfile, app.RestoreOriginal:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *operationCoordinator) finish() {
|
||||
o.active = false
|
||||
o.intent = app.Intent{}
|
||||
@@ -139,6 +199,7 @@ func (o *operationCoordinator) unexpectedClose() {
|
||||
o.currentError = &operationError{
|
||||
status: o.status,
|
||||
detail: "The Application operation stream closed before publishing a terminal result.",
|
||||
intent: o.intent,
|
||||
}
|
||||
o.errorExpanded = false
|
||||
o.errorOffset = 0
|
||||
@@ -191,12 +252,3 @@ func (o *operationCoordinator) requestQuit() tea.Cmd {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user