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}