fix: finalize TUI operation coordination
hermetic-ci / test (push) Canceled after 0s

This commit is contained in:
proxyctl coordinator
2026-07-18 02:56:33 +08:00
parent 652d7eba2b
commit e6fd3ffe73
8 changed files with 153 additions and 67 deletions
+27 -30
View File
@@ -52,28 +52,13 @@ func newOperationCoordinator() operationCoordinator {
}
}
// 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 {
func waitForOperation(updates <-chan app.OperationUpdate) 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
update, ok := <-updates
if !ok {
return operationProgress{updates: updates, closed: true}
}
return operationProgress{update: update, updates: updates}
}
}
@@ -81,7 +66,14 @@ func (o operationCoordinator) wait() tea.Cmd {
if !o.active || o.updates == nil {
return nil
}
return waitForOperation(o.updates, o.spinner.Tick)
return waitForOperation(o.updates)
}
func (o operationCoordinator) commands() tea.Cmd {
if !o.active {
return nil
}
return tea.Batch(o.wait(), o.spinner.Tick)
}
func (o *operationCoordinator) start(application app.InteractiveModule, intent app.Intent) (app.Snapshot, tea.Cmd, bool) {
@@ -99,7 +91,7 @@ func (o *operationCoordinator) start(application app.InteractiveModule, intent a
o.progress[intent.TargetID] = app.TargetProgressRunning
}
o.status = app.OperationStatus{Kind: app.StatusProgress, Summary: operationStartingSummary(intent.Kind)}
return stream.Snapshot, o.wait(), true
return stream.Snapshot, o.commands(), true
}
func operationStartingSummary(kind app.IntentKind) string {
@@ -119,7 +111,7 @@ func (o *operationCoordinator) tick(message spinner.TickMsg) tea.Cmd {
}
updated, nextTick := o.spinner.Update(message)
o.spinner = updated
return waitForOperation(o.updates, nextTick)
return nextTick
}
func (o *operationCoordinator) apply(update app.OperationUpdate) (firstFailedTargetID string) {
@@ -134,9 +126,7 @@ func (o *operationCoordinator) apply(update app.OperationUpdate) (firstFailedTar
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
o.recordError(&operationError{status: decoded.Status, detail: errorDetail(decoded.Err), intent: o.intent})
firstFailedTargetID = decoded.FirstFailedTargetID
case app.OperationConfirmation:
intent := o.intent
@@ -157,6 +147,15 @@ func errorDetail(err error) string {
return err.Error()
}
func (o *operationCoordinator) recordError(candidate *operationError) {
if o.currentError != nil && !relatedResolution(o.currentError.intent, candidate.intent) {
return
}
o.currentError = candidate
o.errorExpanded = false
o.errorOffset = 0
}
func (o *operationCoordinator) resolveError(success app.Intent) {
if o.currentError == nil || !relatedResolution(o.currentError.intent, success) {
return
@@ -196,13 +195,11 @@ func (o *operationCoordinator) finish() {
func (o *operationCoordinator) unexpectedClose() {
o.status = app.OperationStatus{Kind: app.StatusError, Summary: "Operation ended without a final result"}
o.currentError = &operationError{
o.recordError(&operationError{
status: o.status,
detail: "The Application operation stream closed before publishing a terminal result.",
intent: o.intent,
}
o.errorExpanded = false
o.errorOffset = 0
})
o.finish()
}