fix: serialize target panel operations

This commit is contained in:
proxyctl coordinator
2026-07-17 09:33:13 +08:00
parent fb456f35e4
commit e9718a6d08
2 changed files with 224 additions and 12 deletions
+50 -12
View File
@@ -72,8 +72,18 @@ func NewModel(application app.Module) Model {
func (m Model) Init() tea.Cmd { return nil }
// busy is the single-operation guard for asynchronous Application work. Every
// operation start checks it, so a completion can only belong to the one current
// operation rather than racing another operation for shared model state.
func (m Model) busy() bool {
return m.applying || m.refreshing || m.profileTransition || m.pendingActivation.Kind != 0
}
func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
if completed, ok := message.(refreshDone); ok {
if !m.refreshing {
return m, nil
}
m.refreshing = false
m.snapshot = completed.result.Snapshot
if completed.result.Err != nil {
@@ -81,12 +91,20 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
} else {
m.status = completed.result.Notice
}
if m.selected >= len(m.availableTargets()) && m.selected > 0 {
m.selected--
targetCount := len(m.availableTargets())
if targetCount == 0 {
m.selected = 0
} else if m.selected < 0 {
m.selected = 0
} else if m.selected >= targetCount {
m.selected = targetCount - 1
}
return m, nil
}
if progress, ok := message.(profileOperationProgress); ok {
if !m.profileTransition {
return m, nil
}
m.snapshot = progress.result.Snapshot
m.progressTarget = ""
if len(progress.result.Progress) > 0 {
@@ -119,6 +137,9 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, waitForProfileProgress(progress.updates)
}
if completed, ok := message.(targetOperationDone); ok {
if !m.applying || m.profileTransition {
return m, nil
}
m.applying = false
m.snapshot = completed.result.Snapshot
if completed.result.Confirmation != "" {
@@ -159,7 +180,7 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
}
func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.targetConfirm != nil && !m.applying {
if m.targetConfirm != nil && !m.busy() {
switch key.String() {
case "y":
intent := *m.targetConfirm
@@ -177,14 +198,18 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
targets := m.availableTargets()
switch key.String() {
case "p":
m.page = profilesPage
m.selected = 0
m.status = ""
if !m.busy() {
m.page = profilesPage
m.selected = 0
m.status = ""
}
case "?":
m.page = helpPage
m.status = ""
if !m.busy() {
m.page = helpPage
m.status = ""
}
case "r":
if !m.applying && !m.refreshing {
if !m.busy() {
m.refreshing = true
m.status = "Refreshing Target availability and state…"
return m, func() tea.Msg {
@@ -192,15 +217,15 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
}
}
case "up", "k":
if !m.applying && m.selected > 0 {
if !m.busy() && m.selected > 0 {
m.selected--
}
case "down", "j":
if !m.applying && m.selected+1 < len(targets) {
if !m.busy() && m.selected+1 < len(targets) {
m.selected++
}
case " ":
if !m.applying && m.selected >= 0 && m.selected < len(targets) {
if !m.busy() && m.selected >= 0 && m.selected < len(targets) {
selected := targets[m.selected]
desired := selected.State == target.Disabled || selected.State == target.External
m.applying = true
@@ -224,9 +249,11 @@ func (m Model) updateProfiles(key tea.KeyMsg) (tea.Model, tea.Cmd) {
intent := *m.profileConfirm
intent.Confirm = true
m.profileConfirm = nil
m.pendingActivation = app.Intent{}
return m.beginProfileActivation(intent)
case "n", "esc", "q":
m.profileConfirm = nil
m.pendingActivation = app.Intent{}
m.status = ""
}
return m, nil
@@ -243,6 +270,14 @@ func (m Model) updateProfiles(key tea.KeyMsg) (tea.Model, tea.Cmd) {
}
return m, nil
}
if m.busy() {
if key.String() == "q" || key.String() == "esc" {
m.page = mainPage
m.selected = 0
m.status = ""
}
return m, nil
}
switch key.String() {
case "q", "esc":
@@ -332,6 +367,9 @@ func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
}
func (m Model) beginProfileActivation(intent app.Intent) (tea.Model, tea.Cmd) {
if m.busy() {
return m, nil
}
if progressive, ok := m.application.(app.ProgressiveModule); ok {
m.applying = true
m.profileTransition = true