permission.go

  1package dialog
  2
  3import (
  4	"fmt"
  5
  6	"github.com/charmbracelet/bubbles/key"
  7	"github.com/charmbracelet/bubbles/viewport"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/charmbracelet/glamour"
 10	"github.com/charmbracelet/lipgloss"
 11	"github.com/kujtimiihoxha/termai/internal/llm/tools"
 12	"github.com/kujtimiihoxha/termai/internal/permission"
 13	"github.com/kujtimiihoxha/termai/internal/tui/components/core"
 14	"github.com/kujtimiihoxha/termai/internal/tui/layout"
 15	"github.com/kujtimiihoxha/termai/internal/tui/styles"
 16	"github.com/kujtimiihoxha/termai/internal/tui/util"
 17
 18	"github.com/charmbracelet/huh"
 19)
 20
 21type PermissionAction string
 22
 23// Permission responses
 24const (
 25	PermissionAllow           PermissionAction = "allow"
 26	PermissionAllowForSession PermissionAction = "allow_session"
 27	PermissionDeny            PermissionAction = "deny"
 28)
 29
 30// PermissionResponseMsg represents the user's response to a permission request
 31type PermissionResponseMsg struct {
 32	Permission permission.PermissionRequest
 33	Action     PermissionAction
 34}
 35
 36// PermissionDialog interface for permission dialog component
 37type PermissionDialog interface {
 38	tea.Model
 39	layout.Sizeable
 40	layout.Bindings
 41}
 42
 43type keyMap struct {
 44	ChangeFocus key.Binding
 45}
 46
 47var keyMapValue = keyMap{
 48	ChangeFocus: key.NewBinding(
 49		key.WithKeys("tab"),
 50		key.WithHelp("tab", "change focus"),
 51	),
 52}
 53
 54// permissionDialogCmp is the implementation of PermissionDialog
 55type permissionDialogCmp struct {
 56	form            *huh.Form
 57	width           int
 58	height          int
 59	permission      permission.PermissionRequest
 60	windowSize      tea.WindowSizeMsg
 61	r               *glamour.TermRenderer
 62	contentViewPort viewport.Model
 63	isViewportFocus bool
 64	selectOption    *huh.Select[string]
 65}
 66
 67func (p *permissionDialogCmp) Init() tea.Cmd {
 68	return nil
 69}
 70
 71func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 72	var cmds []tea.Cmd
 73
 74	switch msg := msg.(type) {
 75	case tea.WindowSizeMsg:
 76		p.windowSize = msg
 77	case tea.KeyMsg:
 78		if key.Matches(msg, keyMapValue.ChangeFocus) {
 79			p.isViewportFocus = !p.isViewportFocus
 80			if p.isViewportFocus {
 81				p.selectOption.Blur()
 82			} else {
 83				p.selectOption.Focus()
 84			}
 85			return p, nil
 86		}
 87	}
 88
 89	if p.isViewportFocus {
 90		viewPort, cmd := p.contentViewPort.Update(msg)
 91		p.contentViewPort = viewPort
 92		cmds = append(cmds, cmd)
 93	} else {
 94		form, cmd := p.form.Update(msg)
 95		if f, ok := form.(*huh.Form); ok {
 96			p.form = f
 97			cmds = append(cmds, cmd)
 98		}
 99
100		if p.form.State == huh.StateCompleted {
101			// Get the selected action
102			action := p.form.GetString("action")
103
104			// Close the dialog and return the response
105			return p, tea.Batch(
106				util.CmdHandler(core.DialogCloseMsg{}),
107				util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}),
108			)
109		}
110	}
111	return p, tea.Batch(cmds...)
112}
113
114func (p *permissionDialogCmp) render() string {
115	form := p.form.View()
116	keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater)
117	valueStyle := lipgloss.NewStyle().Foreground(styles.Peach)
118
119	headerParts := []string{
120		lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)),
121		" ",
122		lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
123		" ",
124	}
125	r, _ := glamour.NewTermRenderer(
126		glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
127		glamour.WithWordWrap(p.width-10),
128		glamour.WithEmoji(),
129	)
130	content := ""
131	switch p.permission.ToolName {
132	case tools.BashToolName:
133		pr := p.permission.Params.(tools.BashPermissionsParams)
134		headerParts = append(headerParts, keyStyle.Render("Command:"))
135		content, _ = r.Render(fmt.Sprintf("```bash\n%s\n```", pr.Command))
136	case tools.EditToolName:
137		pr := p.permission.Params.(tools.EditPermissionsParams)
138		headerParts = append(headerParts, keyStyle.Render("Update:"))
139		content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Diff))
140	case tools.WriteToolName:
141		pr := p.permission.Params.(tools.WritePermissionsParams)
142		headerParts = append(headerParts, keyStyle.Render("Content:"))
143		content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Content))
144	default:
145		content, _ = r.Render(p.permission.Description)
146	}
147	headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
148	p.contentViewPort.Width = p.width - 2 - 2
149	p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
150	p.contentViewPort.SetContent(content)
151	contentBorder := lipgloss.RoundedBorder()
152	if p.isViewportFocus {
153		contentBorder = lipgloss.DoubleBorder()
154	}
155	cotentStyle := lipgloss.NewStyle().MarginTop(1).Padding(0, 1).Border(contentBorder).BorderForeground(styles.Flamingo)
156
157	return lipgloss.JoinVertical(
158		lipgloss.Top,
159		headerContent,
160		cotentStyle.Render(p.contentViewPort.View()),
161		form,
162	)
163}
164
165func (p *permissionDialogCmp) View() string {
166	return p.render()
167}
168
169func (p *permissionDialogCmp) GetSize() (int, int) {
170	return p.width, p.height
171}
172
173func (p *permissionDialogCmp) SetSize(width int, height int) {
174	p.width = width
175	p.height = height
176	p.form = p.form.WithWidth(width)
177}
178
179func (p *permissionDialogCmp) BindingKeys() []key.Binding {
180	return p.form.KeyBinds()
181}
182
183func newPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialog {
184	// Create a note field for displaying the content
185
186	// Create select field for the permission options
187	selectOption := huh.NewSelect[string]().
188		Key("action").
189		Options(
190			huh.NewOption("Allow", string(PermissionAllow)),
191			huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
192			huh.NewOption("Deny", string(PermissionDeny)),
193		).
194		Title("Select an action")
195
196	// Apply theme
197	theme := styles.HuhTheme()
198
199	// Setup form width and height
200	form := huh.NewForm(huh.NewGroup(selectOption)).
201		WithShowHelp(false).
202		WithTheme(theme).
203		WithShowErrors(false)
204
205	// Focus the form for immediate interaction
206	selectOption.Focus()
207
208	return &permissionDialogCmp{
209		permission:   permission,
210		form:         form,
211		selectOption: selectOption,
212	}
213}
214
215// NewPermissionDialogCmd creates a new permission dialog command
216func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
217	permDialog := newPermissionDialogCmp(permission)
218
219	// Create the dialog layout
220	dialogPane := layout.NewSinglePane(
221		permDialog.(*permissionDialogCmp),
222		layout.WithSinglePaneBordered(true),
223		layout.WithSinglePaneFocusable(true),
224		layout.WithSinglePaneActiveColor(styles.Warning),
225		layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{
226			layout.TopMiddleBorder: " Permission Required ",
227		}),
228	)
229
230	// Focus the dialog
231	dialogPane.Focus()
232	widthRatio := 0.7
233	heightRatio := 0.6
234	minWidth := 100
235	minHeight := 30
236
237	switch permission.ToolName {
238	case tools.BashToolName:
239		widthRatio = 0.5
240		heightRatio = 0.3
241		minWidth = 80
242		minHeight = 20
243	}
244	// Return the dialog command
245	return util.CmdHandler(core.DialogMsg{
246		Content:     dialogPane,
247		WidthRatio:  widthRatio,
248		HeightRatio: heightRatio,
249		MinWidth:    minWidth,
250		MinHeight:   minHeight,
251	})
252}