pi-agent: Implement Help navigation consistency

This commit is contained in:
proxyctl coordinator
2026-07-17 23:33:56 +08:00
parent 112f700650
commit c93dc61ef6
10 changed files with 544 additions and 64 deletions
+24 -14
View File
@@ -19,7 +19,6 @@ const (
mainPage page = iota
profilesPage
formPage
helpPage
)
type formState struct {
@@ -49,6 +48,7 @@ type Model struct {
driftResolutionTargetID string
picker profilePickerState
form formState
help helpDialogState
status string
operation operationCoordinator
detect func(context.Context, string) (probe.Capabilities, error)
@@ -139,12 +139,19 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
if key, ok := message.(tea.KeyMsg); ok {
// q exits the unobscured dashboard. While the searchable picker is open,
// it is ordinary filter input; Escape remains deterministic cancellation.
exitRequest := key.String() == "ctrl+c" || (m.page == mainPage && !m.picker.open && key.String() == "q")
if exitRequest {
// Ctrl+C is global. q exits only the dashboard when no dialog obscures it;
// otherwise the top dialog or child page owns q.
if key.String() == "ctrl+c" || (key.String() == "q" && m.dashboardQuitAvailable()) {
return m, m.operation.requestQuit()
}
// Help is available from every page and dialog. Opening it explicitly
// places it above the current dialog without changing state underneath.
if key.String() == "?" {
if !m.help.top(m.operation.confirmation) {
m.help.show()
}
return m, nil
}
}
if progress, ok := message.(operationProgress); ok {
if !m.operation.active || progress.updates != m.operation.updates {
@@ -166,6 +173,11 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
}
if m.operation.confirmation != nil {
m.page = mainPage
if m.help.open {
// A safety confirmation requested after Help was opened is the new
// top dialog. Cancelling it reveals the unchanged Help beneath.
m.help.confirmationAbove = true
}
}
if progress.update.IsTerminal() {
return m, m.operation.deferredQuit()
@@ -220,6 +232,10 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
if !ok {
return m, nil
}
if m.help.top(m.operation.confirmation) {
m.help.update(key.String())
return m, nil
}
switch m.page {
case mainPage:
return m.updateMain(key)
@@ -227,11 +243,6 @@ func (m Model) Update(message tea.Msg) (tea.Model, tea.Cmd) {
return m.updateProfiles(key)
case formPage:
return m.updateForm(key)
case helpPage:
if key.String() == "q" || key.String() == "esc" {
m.page = mainPage
}
return m, nil
default:
return m, nil
}
@@ -314,7 +325,7 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
}
if m.driftResolutionTargetID != "" {
switch key.String() {
case "enter", "esc", "n":
case "enter", "esc", "n", "q":
m.driftResolutionTargetID = ""
return m, nil
case "r", "o":
@@ -381,8 +392,6 @@ func (m Model) updateMain(key tea.KeyMsg) (tea.Model, tea.Cmd) {
m.operation.status = app.OperationStatus{}
}
}
case "?":
m.page = helpPage
case "r":
if m.operation.active || m.operation.quitRequested {
m.operation.status = app.OperationStatus{Kind: app.StatusBlocked, Summary: "Wait for the current mutation to finish before refreshing Targets"}
@@ -546,7 +555,7 @@ func newProfileDeleteConfirmation(selected profile.Profile, final, active bool)
func (m Model) updateForm(key tea.KeyMsg) (tea.Model, tea.Cmd) {
switch key.String() {
case "esc":
case "q", "esc":
m.page = profilesPage
m.status = ""
m.form = formState{}
@@ -723,6 +732,7 @@ func (m Model) View() string {
driftResolutionTargetID: m.driftResolutionTargetID,
picker: m.picker,
form: m.form,
help: m.help,
confirmation: m.operation.confirmation,
confirmationDialog: &m.operation.confirmationDialog,
status: m.status,