pi-agent: Implement contextual Target actions
This commit is contained in:
+117
-51
@@ -10,7 +10,6 @@ import (
|
||||
"proxyctl/internal/app"
|
||||
"proxyctl/internal/probe"
|
||||
"proxyctl/internal/profile"
|
||||
"proxyctl/internal/target"
|
||||
)
|
||||
|
||||
type page int
|
||||
@@ -31,22 +30,23 @@ type formState struct {
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
application app.InteractiveModule
|
||||
presentation terminalPresentation
|
||||
snapshot app.Snapshot
|
||||
page page
|
||||
width int
|
||||
height int
|
||||
selected int
|
||||
selectedTargetID string
|
||||
dashboardFocus dashboardPane
|
||||
showUnavailable bool
|
||||
detailsOffset int
|
||||
form formState
|
||||
confirm bool
|
||||
status string
|
||||
operation operationCoordinator
|
||||
detect func(context.Context, string) (probe.Capabilities, error)
|
||||
application app.InteractiveModule
|
||||
presentation terminalPresentation
|
||||
snapshot app.Snapshot
|
||||
page page
|
||||
width int
|
||||
height int
|
||||
selected int
|
||||
selectedTargetID string
|
||||
dashboardFocus dashboardPane
|
||||
showUnavailable bool
|
||||
detailsOffset int
|
||||
driftResolutionTargetID string
|
||||
form formState
|
||||
confirm bool
|
||||
status string
|
||||
operation operationCoordinator
|
||||
detect func(context.Context, string) (probe.Capabilities, error)
|
||||
}
|
||||
|
||||
// probeDone carries one finished Protocol Probe back to the form. address is
|
||||
@@ -237,27 +237,54 @@ type mutationAvailability struct {
|
||||
compactReason string
|
||||
}
|
||||
|
||||
// targetMutationAvailability is the sole policy used by both input handling
|
||||
// and rendering, so the displayed disabled reason cannot drift from behavior.
|
||||
func targetMutationAvailability(selected app.TargetSnapshot, selectedOK bool, activeProfile string, operationBlocked bool) mutationAvailability {
|
||||
// targetMutationAvailability consumes the Application's structured action
|
||||
// policy. It adds only Model-owned serialization state and never derives an
|
||||
// action from Target state or display text.
|
||||
func targetMutationAvailability(selected app.TargetSnapshot, selectedOK bool, operationBlocked bool) mutationAvailability {
|
||||
switch {
|
||||
case operationBlocked:
|
||||
return mutationAvailability{reason: "Wait for the current mutation to finish before starting another modification", compactReason: "mutation in progress"}
|
||||
case !selectedOK:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because no Target is selected", compactReason: "no Target selected"}
|
||||
case selected.DetectionPhase == app.TargetChecking:
|
||||
return mutationAvailability{reason: "Target mutation is disabled while this Target is still being checked", compactReason: "checking Target"}
|
||||
case selected.DetectionPhase == app.TargetDetectionFailed:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because this Target check failed; refresh Targets to retry", compactReason: "Target check failed"}
|
||||
case !selected.Available:
|
||||
return mutationAvailability{reason: "Target mutation is disabled because this Target is unavailable", compactReason: "Target unavailable"}
|
||||
case (selected.State == target.Disabled || selected.State == target.External) && activeProfile == "":
|
||||
return mutationAvailability{reason: "Target mutation is disabled because there is no Active Profile; open Profile management first", compactReason: "no Active Profile"}
|
||||
case !selected.PrimaryAction.Available:
|
||||
reason := selected.PrimaryAction.DisabledReason
|
||||
if reason == "" {
|
||||
reason = "This Target action is unavailable"
|
||||
}
|
||||
return mutationAvailability{reason: reason, compactReason: "unavailable"}
|
||||
default:
|
||||
return mutationAvailability{available: true}
|
||||
}
|
||||
}
|
||||
|
||||
func driftResolutionAction(selected app.TargetSnapshot, kind app.TargetActionKind) (app.TargetAction, bool) {
|
||||
for _, action := range selected.DriftResolutionActions {
|
||||
if action.Kind == kind {
|
||||
return action, true
|
||||
}
|
||||
}
|
||||
return app.TargetAction{Kind: kind}, false
|
||||
}
|
||||
|
||||
func targetIntent(kind app.TargetActionKind, targetID string) (app.Intent, bool) {
|
||||
intent := app.Intent{TargetID: targetID}
|
||||
switch kind {
|
||||
case app.EnableTargetAction:
|
||||
intent.Kind = app.EnableTarget
|
||||
case app.DisableTargetAction:
|
||||
intent.Kind = app.DisableTarget
|
||||
case app.ApplyProfileTargetAction:
|
||||
intent.Kind = app.ApplyProfileToTarget
|
||||
case app.ReapplyActiveProfileAction:
|
||||
intent.Kind = app.ReapplyActiveProfile
|
||||
case app.RestoreOriginalAction:
|
||||
intent.Kind = app.RestoreOriginal
|
||||
default:
|
||||
return app.Intent{}, false
|
||||
}
|
||||
return intent, true
|
||||
}
|
||||
|
||||
func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.operation.confirmation != nil {
|
||||
switch key.String() {
|
||||
@@ -273,6 +300,37 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.driftResolutionTargetID != "" {
|
||||
switch key.String() {
|
||||
case "enter", "esc", "n":
|
||||
m.driftResolutionTargetID = ""
|
||||
return m, nil
|
||||
case "r", "o":
|
||||
kind := app.ReapplyActiveProfileAction
|
||||
if key.String() == "o" {
|
||||
kind = app.RestoreOriginalAction
|
||||
}
|
||||
selected, ok := newTargetCatalog(m.snapshot, true, m.driftResolutionTargetID).selected(m.driftResolutionTargetID)
|
||||
if !ok {
|
||||
m.driftResolutionTargetID = ""
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Drift Resolution is unavailable because the Target is no longer present"}
|
||||
return m, nil
|
||||
}
|
||||
action, ok := driftResolutionAction(selected, kind)
|
||||
if !ok || !action.Available {
|
||||
reason := action.DisabledReason
|
||||
if reason == "" {
|
||||
reason = "This Drift Resolution action is unavailable"
|
||||
}
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: reason}
|
||||
return m, nil
|
||||
}
|
||||
intent, _ := targetIntent(kind, selected.ID)
|
||||
m.driftResolutionTargetID = ""
|
||||
return m, m.startOperation(intent)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if key.String() == "e" && m.operation.currentError != nil {
|
||||
m.operation.errorExpanded = !m.operation.errorExpanded
|
||||
m.operation.errorOffset = 0
|
||||
@@ -356,13 +414,20 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case " ":
|
||||
selected, ok := catalog.selected(m.selectedTargetID)
|
||||
availability := targetMutationAvailability(selected, ok, m.snapshot.ActiveProfile, m.operation.active || m.operation.quitRequested)
|
||||
availability := targetMutationAvailability(selected, ok, m.operation.active || m.operation.quitRequested)
|
||||
if !availability.available {
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: availability.reason}
|
||||
break
|
||||
}
|
||||
desired := selected.State == target.Disabled || selected.State == target.External
|
||||
intent := app.Intent{Kind: app.SetTargetDesired, TargetID: selected.ID, Enabled: desired}
|
||||
if selected.PrimaryAction.Kind == app.ResolveDriftTargetAction {
|
||||
m.driftResolutionTargetID = selected.ID
|
||||
break
|
||||
}
|
||||
intent, ok := targetIntent(selected.PrimaryAction.Kind, selected.ID)
|
||||
if !ok {
|
||||
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "This Target action is unavailable"}
|
||||
break
|
||||
}
|
||||
return m, m.startOperation(intent)
|
||||
}
|
||||
|
||||
@@ -553,26 +618,27 @@ func (m *Model) selectName(name string) {
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.presentation.Render(presentationState{
|
||||
snapshot: m.snapshot,
|
||||
page: m.page,
|
||||
width: m.width,
|
||||
height: m.height,
|
||||
selected: m.selected,
|
||||
selectedTargetID: m.selectedTargetID,
|
||||
dashboardFocus: m.dashboardFocus,
|
||||
showUnavailable: m.showUnavailable,
|
||||
detailsOffset: m.detailsOffset,
|
||||
form: m.form,
|
||||
confirm: m.confirm,
|
||||
confirmation: m.operation.confirmation,
|
||||
status: m.status,
|
||||
operationStatus: m.operation.status,
|
||||
currentError: m.operation.currentError,
|
||||
errorExpanded: m.operation.errorExpanded,
|
||||
errorOffset: m.operation.errorOffset,
|
||||
exitDeferred: m.operation.quitRequested && m.operationInFlight(),
|
||||
operationActive: m.operation.active,
|
||||
operationProgress: m.operation.progress,
|
||||
snapshot: m.snapshot,
|
||||
page: m.page,
|
||||
width: m.width,
|
||||
height: m.height,
|
||||
selected: m.selected,
|
||||
selectedTargetID: m.selectedTargetID,
|
||||
dashboardFocus: m.dashboardFocus,
|
||||
showUnavailable: m.showUnavailable,
|
||||
detailsOffset: m.detailsOffset,
|
||||
driftResolutionTargetID: m.driftResolutionTargetID,
|
||||
form: m.form,
|
||||
confirm: m.confirm,
|
||||
confirmation: m.operation.confirmation,
|
||||
status: m.status,
|
||||
operationStatus: m.operation.status,
|
||||
currentError: m.operation.currentError,
|
||||
errorExpanded: m.operation.errorExpanded,
|
||||
errorOffset: m.operation.errorOffset,
|
||||
exitDeferred: m.operation.quitRequested && m.operationInFlight(),
|
||||
operationActive: m.operation.active,
|
||||
operationProgress: m.operation.progress,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user