feat(tui-go): grant scope picker (A) + standing-grants viewer
Surface the new cross-session grant scopes in the TUI. - Approve-always (A) now opens a scope picker instead of immediately creating a SESSION grant: choose this session / this project / everywhere. SESSION stays the default (A then enter/s = old behaviour); p and g create PROJECT / GLOBAL grants. The grant + approval are sent on confirm (esc cancels, leaving the call pending). - New "grants" palette command + G shortcut open a standing-grants viewer (OverlayGrants) listing the active PROJECT/GLOBAL grants — scope, tool, permitted tiers, project path — with x/enter to revoke. The server's RevokeGrant reply (a fresh grant.list) refreshes it in place. - protocol.go: TypeGrantList + GrantDto; RevokeGrant/ListGrants encoders; CreateGrant now documents the wider scopes. server.go decodes grant.list into m.grants and treats it as a non-session global reply. - render-matrix coverage: grant.list classified as a non-rendering query reply (populates the overlay, not the transcript). - demo.go: grants + grant-scope preview kinds. go build / vet / test green; rendered both overlays via cmd/preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,10 @@ func (m Model) renderOverlay(base string) string {
|
||||
return m.center(m.filesModal())
|
||||
case OverlayStatusbar:
|
||||
return m.center(m.statusbarModal())
|
||||
case OverlayGrants:
|
||||
return m.center(m.grantsModal())
|
||||
case OverlayGrantScope:
|
||||
return m.center(m.grantScopeModal())
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -615,6 +619,75 @@ func (m Model) statusbarModal() string {
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
// grantsModal lists the active standing (PROJECT/GLOBAL) grants and lets the operator revoke one.
|
||||
// These are cross-session auto-approvals, so the board opens from anywhere.
|
||||
func (m Model) grantsModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(m.titleLine("standing grants") + "\n\n")
|
||||
b.WriteString(mbg(t, " cross-session auto-approvals in force (tool-bound)", t.P.Faint) + "\n\n")
|
||||
|
||||
switch {
|
||||
case m.grantsLoading:
|
||||
b.WriteString(mbg(t, " loading…", t.P.Faint) + "\n")
|
||||
case len(m.grants) == 0:
|
||||
b.WriteString(mbg(t, " none — press A on an approval to grant project / global", t.P.Faint) + "\n")
|
||||
default:
|
||||
for i, g := range m.grants {
|
||||
marker := mbg(t, " ", t.P.BgPanel)
|
||||
fg := t.P.Fg
|
||||
if i == m.grantIndex {
|
||||
marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▌ ")
|
||||
fg = t.P.FgStrong
|
||||
}
|
||||
scope := lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.BgPanel).Render(padRaw(g.Scope, 8))
|
||||
tool := lipgloss.NewStyle().Foreground(fg).Background(t.P.BgPanel).Render(padRaw(g.ToolName, 16))
|
||||
tiers := mbg(t, "≤"+strings.Join(g.Tiers, ","), t.P.Faint)
|
||||
b.WriteString(marker + scope + tool + tiers)
|
||||
if g.Scope == "PROJECT" && g.ProjectID != "" {
|
||||
b.WriteString(mbg(t, " "+clip(g.ProjectID, w-52), t.P.Faint))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"x", "revoke"}, {"G/esc", "close"}}))
|
||||
modal := t.Overlay.Width(w).Render(b.String())
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
// grantScopeModal is the A (approve-always) breadth picker: choose how widely to stop being asked
|
||||
// for the pending tool — this session, this project, or everywhere.
|
||||
func (m Model) grantScopeModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
tool := ""
|
||||
if m.grantFor != nil {
|
||||
tool = m.grantFor.ToolName
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(m.titleLine("approve always — choose scope") + "\n\n")
|
||||
b.WriteString(mbg(t, " stop asking for ", t.P.Faint) +
|
||||
lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.BgPanel).Render(tool) + "\n\n")
|
||||
|
||||
for i, opt := range grantScopeOptions() {
|
||||
marker := mbg(t, " ", t.P.BgPanel)
|
||||
labelFg := t.P.Fg
|
||||
if i == m.grantScopeIndex {
|
||||
marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▌ ")
|
||||
labelFg = t.P.FgStrong
|
||||
}
|
||||
key := lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.BgPanel).Render(padRaw(opt.key, 3))
|
||||
title := lipgloss.NewStyle().Foreground(labelFg).Background(t.P.BgPanel).Render(padRaw(opt.title, 16))
|
||||
b.WriteString(marker + key + title + mbg(t, opt.hint, t.P.Faint) + "\n")
|
||||
}
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "move"}, {"enter", "grant"}, {"esc", "cancel"}}))
|
||||
modal := t.Overlay.Width(w).Render(b.String())
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
func (m Model) toolPaletteModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
Reference in New Issue
Block a user