diff --git a/apps/tui-go/internal/app/demo.go b/apps/tui-go/internal/app/demo.go index 9c88fea6..da07f750 100644 --- a/apps/tui-go/internal/app/demo.go +++ b/apps/tui-go/internal/app/demo.go @@ -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" diff --git a/apps/tui-go/internal/app/overlays.go b/apps/tui-go/internal/app/overlays.go index 32ebd026..d482a028 100644 --- a/apps/tui-go/internal/app/overlays.go +++ b/apps/tui-go/internal/app/overlays.go @@ -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"))