feat: permission mode switching in TUI

- Shift+Tab cycles permission modes: bypass → default → plan →
  accept_edits → auto → bypass
- /permission <mode> slash command to set specific mode
- Current mode shown in status bar (🛡 bypass)
- Permission checker wired into TUI config
This commit is contained in:
2026-04-03 16:18:03 +02:00
parent 97b065596d
commit 603e67a77e
2 changed files with 58 additions and 5 deletions

View File

@@ -218,8 +218,9 @@ func main() {
defer sess.Close()
m := tui.New(sess, tui.Config{
Firewall: fw,
Engine: eng,
Firewall: fw,
Engine: eng,
Permissions: permChecker,
})
p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {

View File

@@ -11,6 +11,7 @@ import (
"charm.land/bubbles/v2/textinput"
"charm.land/lipgloss/v2"
"somegit.dev/Owlibou/gnoma/internal/engine"
"somegit.dev/Owlibou/gnoma/internal/permission"
"somegit.dev/Owlibou/gnoma/internal/security"
"somegit.dev/Owlibou/gnoma/internal/session"
"somegit.dev/Owlibou/gnoma/internal/stream"
@@ -28,8 +29,9 @@ type chatMessage struct {
// Config holds optional dependencies for TUI features.
type Config struct {
Firewall *security.Firewall // for incognito toggle
Engine *engine.Engine // for model switching
Firewall *security.Firewall // for incognito toggle
Engine *engine.Engine // for model switching
Permissions *permission.Checker // for mode switching
}
type Model struct {
@@ -96,6 +98,31 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.session.Cancel()
return m, nil
}
case "shift+tab":
// Cycle permission mode: bypass → default → plan → bypass
if m.config.Permissions != nil {
mode := m.config.Permissions.Mode()
var next permission.Mode
switch mode {
case permission.ModeBypass:
next = permission.ModeDefault
case permission.ModeDefault:
next = permission.ModePlan
case permission.ModePlan:
next = permission.ModeAcceptEdits
case permission.ModeAcceptEdits:
next = permission.ModeAuto
case permission.ModeAuto:
next = permission.ModeBypass
default:
next = permission.ModeBypass
}
m.config.Permissions.SetMode(next)
m.messages = append(m.messages, chatMessage{role: "system",
content: fmt.Sprintf("permission mode: %s", next)})
m.scrollOffset = 0
}
return m, nil
case "pgup", "shift+up":
m.scrollOffset += 5
return m, nil
@@ -219,6 +246,27 @@ func (m Model) handleCommand(cmd string) (tea.Model, tea.Cmd) {
}
return m, nil
case "/permission", "/perm":
if m.config.Permissions == nil {
m.messages = append(m.messages, chatMessage{role: "error", content: "permission checker not configured"})
return m, nil
}
if args == "" {
m.messages = append(m.messages, chatMessage{role: "system",
content: fmt.Sprintf("permission mode: %s\nUsage: /permission <mode> (bypass, default, plan, accept_edits, deny, auto)\nOr press Shift+Tab to cycle", m.config.Permissions.Mode())})
return m, nil
}
mode := permission.Mode(args)
if !mode.Valid() {
m.messages = append(m.messages, chatMessage{role: "error",
content: fmt.Sprintf("invalid mode: %s (valid: bypass, default, plan, accept_edits, deny, auto)", args)})
return m, nil
}
m.config.Permissions.SetMode(mode)
m.messages = append(m.messages, chatMessage{role: "system",
content: fmt.Sprintf("permission mode: %s", mode)})
return m, nil
case "/provider":
if args == "" {
status := m.session.Status()
@@ -459,9 +507,13 @@ func (m Model) renderStatus() string {
}
left := sStatusHighlight.Render(provModel)
// Center: cwd + git branch
// Center: cwd + git branch + perm mode
dir := filepath.Base(m.cwd)
centerParts := []string{"📁 " + dir}
if m.config.Permissions != nil {
mode := string(m.config.Permissions.Mode())
centerParts = append(centerParts, sStatusDim.Render(" 🛡 "+mode))
}
if m.gitBranch != "" {
centerParts = append(centerParts, sStatusBranch.Render(" "+m.gitBranch))
}