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}