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:
@@ -125,7 +125,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
case protocol.TypeSessionResumed:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
s.Status = "ACTIVE"
|
||||
s.Pending = nil
|
||||
s.clearApprovals()
|
||||
s.LastEventAt = nowMillis()
|
||||
}
|
||||
case protocol.TypeSessionCompleted:
|
||||
@@ -134,6 +134,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
s.Active = false
|
||||
s.Clar = nil
|
||||
s.Propose = nil
|
||||
s.clearApprovals()
|
||||
}
|
||||
case protocol.TypeSessionFailed:
|
||||
m.touch(msg.SessionID, "FAILED")
|
||||
@@ -141,6 +142,7 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
s.Active = false
|
||||
s.Clar = nil
|
||||
s.Propose = nil
|
||||
s.clearApprovals()
|
||||
}
|
||||
case protocol.TypeStageStarted:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
@@ -252,7 +254,9 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
m.onWorkflowProposed(msg)
|
||||
case protocol.TypeApprovalResolved:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
s.Pending = nil
|
||||
// Drop just the resolved gate; any others stay queued and the band
|
||||
// advances to the next rather than vanishing entirely.
|
||||
s.removeApproval(msg.RequestID)
|
||||
detail := msg.Outcome
|
||||
if msg.Reason != "" {
|
||||
detail += " — " + msg.Reason
|
||||
@@ -411,7 +415,7 @@ func (m *Model) onApprovalRequired(msg protocol.ServerMessage) {
|
||||
Rationale: rationale,
|
||||
}
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
s.Pending = info
|
||||
s.enqueueApproval(info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,25 +457,25 @@ func (m *Model) onWorkflowProposed(msg protocol.ServerMessage) {
|
||||
}
|
||||
|
||||
func (m *Model) onSnapshot(msg protocol.ServerMessage) {
|
||||
var pending *Approval
|
||||
if len(msg.PendingAppr) > 0 {
|
||||
a := msg.PendingAppr[0]
|
||||
pending = &Approval{
|
||||
var queue []*Approval
|
||||
for _, a := range msg.PendingAppr {
|
||||
queue = append(queue, &Approval{
|
||||
RequestID: a.RequestID, SessionID: msg.SessionID, Tier: a.Tier,
|
||||
Risk: "unknown", ToolName: derefp(a.ToolName), Preview: derefp(a.Preview),
|
||||
}
|
||||
})
|
||||
}
|
||||
status := "running"
|
||||
if msg.State != nil {
|
||||
status = msg.State.Status
|
||||
}
|
||||
if pending != nil {
|
||||
if len(queue) > 0 {
|
||||
status = "PAUSED awaiting approval"
|
||||
}
|
||||
sess := Session{
|
||||
ID: msg.SessionID, Status: status, WorkflowID: msg.WorkflowID,
|
||||
Name: msg.WorkflowID, LastEventAt: nowMillis(), Pending: pending,
|
||||
Name: msg.WorkflowID, LastEventAt: nowMillis(), PendingQueue: queue,
|
||||
}
|
||||
sess.syncPending()
|
||||
if msg.State != nil && msg.State.CurrentStageID != nil {
|
||||
sess.CurrentStage = *msg.State.CurrentStageID
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user