From 603e67a77ea8e0553c6e9ec13f458478967298bc Mon Sep 17 00:00:00 2001 From: vikingowl Date: Fri, 3 Apr 2026 16:18:03 +0200 Subject: [PATCH] feat: permission mode switching in TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Shift+Tab cycles permission modes: bypass → default → plan → accept_edits → auto → bypass - /permission slash command to set specific mode - Current mode shown in status bar (🛡 bypass) - Permission checker wired into TUI config --- cmd/gnoma/main.go | 5 ++-- internal/tui/app.go | 58 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/cmd/gnoma/main.go b/cmd/gnoma/main.go index 9c67f36..43b1bc0 100644 --- a/cmd/gnoma/main.go +++ b/cmd/gnoma/main.go @@ -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 { diff --git a/internal/tui/app.go b/internal/tui/app.go index 0e78451..5c2185e 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -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 (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)) }