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	keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater)
116	valueStyle := lipgloss.NewStyle().Foreground(styles.Peach)
117
118	form := p.form.View()
119
120	headerParts := []string{
121		lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)),
122		" ",
123		lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
124		" ",
125	}
126	r, _ := glamour.NewTermRenderer(
127		glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
128		glamour.WithWordWrap(p.width-10),
129		glamour.WithEmoji(),
130	)
131	content := ""
132	switch p.permission.ToolName {
133	case tools.BashToolName:
134		pr := p.permission.Params.(tools.BashPermissionsParams)
135		headerParts = append(headerParts, keyStyle.Render("Command:"))
136		content, _ = r.Render(fmt.Sprintf("```bash\n%s\n```", pr.Command))
137	case tools.EditToolName:
138		pr := p.permission.Params.(tools.EditPermissionsParams)
139		headerParts = append(headerParts, keyStyle.Render("Update"))
140		content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Diff))
141	case tools.WriteToolName:
142		pr := p.permission.Params.(tools.WritePermissionsParams)
143		headerParts = append(headerParts, keyStyle.Render("Content"))
144		content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Content))
145	case tools.FetchToolName:
146		pr := p.permission.Params.(tools.FetchPermissionsParams)
147		headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
148	default:
149		content, _ = r.Render(p.permission.Description)
150	}
151	headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
152	p.contentViewPort.Width = p.width - 2 - 2
153	p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
154	p.contentViewPort.SetContent(content)
155	contentBorder := lipgloss.RoundedBorder()
156	if p.isViewportFocus {
157		contentBorder = lipgloss.DoubleBorder()
158	}
159	cotentStyle := lipgloss.NewStyle().MarginTop(1).Padding(0, 1).Border(contentBorder).BorderForeground(styles.Flamingo)
160	contentFinal := cotentStyle.Render(p.contentViewPort.View())
161	if content == "" {
162		contentFinal = ""
163	}
164	return lipgloss.JoinVertical(
165		lipgloss.Top,
166		headerContent,
167		contentFinal,
168		form,
169	)
170}
171
172func (p *permissionDialogCmp) View() string {
173	return p.render()
174}
175
176func (p *permissionDialogCmp) GetSize() (int, int) {
177	return p.width, p.height
178}
179
180func (p *permissionDialogCmp) SetSize(width int, height int) {
181	p.width = width
182	p.height = height
183	p.form = p.form.WithWidth(width)
184}
185
186func (p *permissionDialogCmp) BindingKeys() []key.Binding {
187	return p.form.KeyBinds()
188}
189
190func newPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialog {
191	// Create a note field for displaying the content
192
193	// Create select field for the permission options
194	selectOption := huh.NewSelect[string]().
195		Key("action").
196		Options(
197			huh.NewOption("Allow", string(PermissionAllow)),
198			huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
199			huh.NewOption("Deny", string(PermissionDeny)),
200		).
201		Title("Select an action")
202
203	// Apply theme
204	theme := styles.HuhTheme()
205
206	// Setup form width and height
207	form := huh.NewForm(huh.NewGroup(selectOption)).
208		WithShowHelp(false).
209		WithTheme(theme).
210		WithShowErrors(false)
211
212	// Focus the form for immediate interaction
213	selectOption.Focus()
214
215	return &permissionDialogCmp{
216		permission:   permission,
217		form:         form,
218		selectOption: selectOption,
219	}
220}
221
222// NewPermissionDialogCmd creates a new permission dialog command
223func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
224	permDialog := newPermissionDialogCmp(permission)
225
226	// Create the dialog layout
227	dialogPane := layout.NewSinglePane(
228		permDialog.(*permissionDialogCmp),
229		layout.WithSinglePaneBordered(true),
230		layout.WithSinglePaneFocusable(true),
231		layout.WithSinglePaneActiveColor(styles.Warning),
232		layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{
233			layout.TopMiddleBorder: " Permission Required ",
234		}),
235	)
236
237	// Focus the dialog
238	dialogPane.Focus()
239	widthRatio := 0.7
240	heightRatio := 0.6
241	minWidth := 100
242	minHeight := 30
243
244	switch permission.ToolName {
245	case tools.BashToolName:
246		widthRatio = 0.5
247		heightRatio = 0.3
248		minWidth = 80
249		minHeight = 20
250	}
251	// Return the dialog command
252	return util.CmdHandler(core.DialogMsg{
253		Content:     dialogPane,
254		WidthRatio:  widthRatio,
255		HeightRatio: heightRatio,
256		MinWidth:    minWidth,
257		MinHeight:   minHeight,
258	})
259}