feat(tui-go): approval ergonomics — queue + tier-gated confirm (E §3)

- pending approvals are a navigable queue (PendingQueue/PendingIdx; ↑↓/jk, count
  shown), no modal stacking; resolve removes the specific gate by request id
- y/n/e keys (approve/reject/steer); T0–T2 act on one key, T3+ requires arm+confirm
  ("press y again"), any other key disarms; reject/steer/auto never gated
- snapshot restore now rehydrates the full pending list, not just the first

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 16:40:49 +00:00
parent 35596dc723
commit d5afcd345a
6 changed files with 483 additions and 53 deletions
+20 -2
View File
@@ -132,6 +132,13 @@ func (m Model) renderApprovalBand(h int) string {
rightHdr := t.span("tier ", t.P.Faint) + t.span(a.Tier, t.P.Accent2) +
t.span(" · risk ", t.P.Faint) +
lipgloss.NewStyle().Foreground(riskColor).Background(t.P.Bg).Bold(true).Render(strings.ToUpper(a.Risk))
// When more than one gate is queued, show "approval i/n" so the operator knows
// there are others behind this one (↑/↓ to walk them) — never stack modals.
if n := len(s.PendingQueue); n > 1 {
rightHdr = t.span("approval ", t.P.Faint) +
t.span(itoa(s.PendingIdx+1)+"/"+itoa(n), t.P.Accent) +
t.span(" · ", t.P.Faint) + rightHdr
}
innerH := h - 2
ratBlock := 0
@@ -218,8 +225,19 @@ func (m Model) approvalActions() string {
hint("enter", "approve + send note"), hint("esc", "keep note, decide below"),
}, gap)
}
parts := []string{hint("a", "approve"), hint("A", "auto"), hint("s", "steer"), hint("r", "reject")}
if s := m.session(m.selectedID); s != nil && s.Pending != nil &&
s := m.session(m.selectedID)
// Armed T3+ approve: a single decisive line so an accidental keystroke can't slip
// a destructive action through — the confirm key must be pressed again.
if m.approvalArmed && s != nil && s.Pending != nil {
warn := lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.Bg).Bold(true).
Render("press y again to confirm " + s.Pending.Tier + " approval")
return warn + gap + hint("any other key", "cancel")
}
parts := []string{hint("y", "approve"), hint("n", "reject"), hint("e", "steer"), hint("A", "auto")}
if s != nil && len(s.PendingQueue) > 1 {
parts = append(parts, hint("↑↓", "queue"))
}
if s != nil && s.Pending != nil &&
(isUnifiedDiff(s.Pending.Preview) || isCommandApproval(s.Pending)) {
parts = append(parts, hint("^x", "fullscreen"))
}