fix(tui): keep the steering note visible in the approval band

The steer note replaced the action row while editing and vanished once
you esc'd out — so you couldn't see the note or the approve/reject keys,
and it was unclear the note still applied (it did: decide() reads the
buffer). Now the note is a persistent line above the action row (editable
with a caret while typing, a dim reminder once set), and the action row
stays visible. Editing hints clarify enter=approve+send, esc=keep note.
This commit is contained in:
2026-06-03 01:49:24 +04:00
parent d8e36b8282
commit c63929d79f
2 changed files with 56 additions and 13 deletions
+16
View File
@@ -84,6 +84,22 @@ func PreviewFrame(kind string, w, h int) string {
}
}
case "approval-steer":
m.connected = true
m.currentModel = "llama-cpp:default"
m.sessions = sampleSessions()
m.selectedID = "04a546aa"
m.sessionEntered = true
m.steerBuffer = "also print the current distro and kernel version"
if s := m.session("04a546aa"); s != nil {
s.CurrentStage = "write_script"
s.Pending = &Approval{
RequestID: "req-3", SessionID: "04a546aa", Tier: "T3", Risk: "MEDIUM",
ToolName: "file_write",
Preview: "--- a/healthcheck.sh\n+++ b/healthcheck.sh\n@@ -0,0 +1,2 @@\n+#!/usr/bin/env bash\n+echo ok\n",
}
}
case "approval-shell":
m.connected = true
m.currentModel = "llama-cpp:default"
+40 -13
View File
@@ -63,7 +63,11 @@ func (m Model) approvalBandHeight() int {
rat = n + 1 // rationale lines + trailing blank
}
}
h := 2 + 1 + 1 + rat + rows + 1 + 1
steer := 0
if m.steering || m.steerBuffer != "" {
steer = 1
}
h := 2 + 1 + 1 + rat + rows + 1 + steer + 1
if maxH := m.height * 55 / 100; h > maxH {
h = maxH
}
@@ -110,7 +114,11 @@ func (m Model) renderApprovalBand(h int) string {
if len(a.Rationale) > 0 {
ratBlock = len(a.Rationale) + 1 // rationale lines + trailing blank
}
diffH := innerH - 4 - ratBlock // header, blank, blank, action row, + rationale block
steerBlock := 0
if m.steering || m.steerBuffer != "" {
steerBlock = 1
}
diffH := innerH - 4 - ratBlock - steerBlock // header, blank, blank, action row, + rationale/steer
if diffH < 1 {
diffH = 1
}
@@ -134,26 +142,45 @@ func (m Model) renderApprovalBand(h int) string {
for len(body) < diffStart+diffH {
body = append(body, "")
}
body = append(body, "", m.approvalActions())
body = append(body, "")
if steerBlock == 1 {
body = append(body, m.steerLine())
}
body = append(body, m.approvalActions())
return t.box("permission required", body, m.width, h, true)
}
// approvalActions renders the action row of the approval band, or the live steer
// note when the operator is composing one.
// steerLine renders the steering note inside the band: editable (with a caret)
// while the operator is typing it, or as a persistent reminder once set — so it
// stays visible after editing and it's clear it rides along with approve/reject.
func (m Model) steerLine() string {
t := m.theme
label := t.span("steer ", t.P.Faint)
caret := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▏")
if m.steering {
if m.steerBuffer == "" {
return label + caret + t.span(" type a note…", t.P.Faint)
}
return label + t.span(m.steerBuffer, t.P.FgStrong) + caret
}
return label + t.span(m.steerBuffer, t.P.Accent2) +
t.span(" (sent with your decision · s to edit)", t.P.Faint)
}
// approvalActions renders the action row of the approval band. While the operator
// is composing a steer note the keys are different (typing is captured), so the
// hints switch to the editing controls.
func (m Model) approvalActions() string {
t := m.theme
if m.steering {
note, fg := m.steerBuffer, t.P.FgStrong
if note == "" {
note, fg = "(steer note — enter to send, esc to cancel)", t.P.Faint
}
return t.span("steer ", t.P.Faint) + t.span(note, fg) +
lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▏")
}
hint := func(k, l string) string {
return lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render(k) +
t.span(" "+l, t.P.Faint)
}
if m.steering {
return strings.Join([]string{
hint("enter", "approve + send note"), hint("esc", "keep note, decide below"),
}, t.span(" ", t.P.Faint))
}
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 && isUnifiedDiff(s.Pending.Preview) {
parts = append(parts, hint("^x", "fullscreen"))