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 // Add a visual indicator for focus change
83 cmds = append(cmds, tea.Batch(
84 util.CmdHandler(util.InfoMsg("Viewing content - use arrow keys to scroll")),
85 ))
86 } else {
87 p.selectOption.Focus()
88 // Add a visual indicator for focus change
89 cmds = append(cmds, tea.Batch(
90 util.CmdHandler(util.InfoMsg("Select an action")),
91 ))
92 }
93 return p, tea.Batch(cmds...)
94 }
95 }
96
97 if p.isViewportFocus {
98 viewPort, cmd := p.contentViewPort.Update(msg)
99 p.contentViewPort = viewPort
100 cmds = append(cmds, cmd)
101 } else {
102 form, cmd := p.form.Update(msg)
103 if f, ok := form.(*huh.Form); ok {
104 p.form = f
105 cmds = append(cmds, cmd)
106 }
107
108 if p.form.State == huh.StateCompleted {
109 // Get the selected action
110 action := p.form.GetString("action")
111
112 // Close the dialog and return the response
113 return p, tea.Batch(
114 util.CmdHandler(core.DialogCloseMsg{}),
115 util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}),
116 )
117 }
118 }
119 return p, tea.Batch(cmds...)
120}
121
122func (p *permissionDialogCmp) render() string {
123 keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater)
124 valueStyle := lipgloss.NewStyle().Foreground(styles.Peach)
125
126 form := p.form.View()
127
128 headerParts := []string{
129 lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)),
130 " ",
131 lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
132 " ",
133 }
134 r, _ := glamour.NewTermRenderer(
135 glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
136 glamour.WithWordWrap(p.width-10),
137 glamour.WithEmoji(),
138 )
139 content := ""
140 switch p.permission.ToolName {
141 case tools.BashToolName:
142 pr := p.permission.Params.(tools.BashPermissionsParams)
143 headerParts = append(headerParts, keyStyle.Render("Command:"))
144 content = fmt.Sprintf("```bash\n%s\n```", pr.Command)
145 case tools.EditToolName:
146 pr := p.permission.Params.(tools.EditPermissionsParams)
147 headerParts = append(headerParts, keyStyle.Render("Update"))
148 content = fmt.Sprintf("```\n%s\n```", pr.Diff)
149 case tools.WriteToolName:
150 pr := p.permission.Params.(tools.WritePermissionsParams)
151 headerParts = append(headerParts, keyStyle.Render("Content"))
152 content = fmt.Sprintf("```\n%s\n```", pr.Content)
153 case tools.FetchToolName:
154 pr := p.permission.Params.(tools.FetchPermissionsParams)
155 headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
156 default:
157 content = p.permission.Description
158 }
159
160 renderedContent, _ := r.Render(content)
161 headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
162 p.contentViewPort.Width = p.width - 2 - 2
163 p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
164 p.contentViewPort.SetContent(renderedContent)
165
166 // Make focus change more apparent with different border styles and colors
167 var contentBorder lipgloss.Border
168 var borderColor lipgloss.TerminalColor
169
170 if p.isViewportFocus {
171 contentBorder = lipgloss.DoubleBorder()
172 borderColor = styles.Blue
173 } else {
174 contentBorder = lipgloss.RoundedBorder()
175 borderColor = styles.Flamingo
176 }
177
178 contentStyle := lipgloss.NewStyle().
179 MarginTop(1).
180 Padding(0, 1).
181 Border(contentBorder).
182 BorderForeground(borderColor)
183
184 if p.isViewportFocus {
185 contentStyle = contentStyle.BorderBackground(styles.Surface0)
186 }
187
188 contentFinal := contentStyle.Render(p.contentViewPort.View())
189 if renderedContent == "" {
190 contentFinal = ""
191 }
192
193 return lipgloss.JoinVertical(
194 lipgloss.Top,
195 headerContent,
196 contentFinal,
197 form,
198 )
199}
200
201func (p *permissionDialogCmp) View() string {
202 return p.render()
203}
204
205func (p *permissionDialogCmp) GetSize() (int, int) {
206 return p.width, p.height
207}
208
209func (p *permissionDialogCmp) SetSize(width int, height int) {
210 p.width = width
211 p.height = height
212 p.form = p.form.WithWidth(width)
213}
214
215func (p *permissionDialogCmp) BindingKeys() []key.Binding {
216 return p.form.KeyBinds()
217}
218
219func newPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialog {
220 // Create a note field for displaying the content
221
222 // Create select field for the permission options
223 selectOption := huh.NewSelect[string]().
224 Key("action").
225 Options(
226 huh.NewOption("Allow", string(PermissionAllow)),
227 huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
228 huh.NewOption("Deny", string(PermissionDeny)),
229 ).
230 Title("Select an action")
231
232 // Apply theme
233 theme := styles.HuhTheme()
234
235 // Setup form width and height
236 form := huh.NewForm(huh.NewGroup(selectOption)).
237 WithShowHelp(false).
238 WithTheme(theme).
239 WithShowErrors(false)
240
241 // Focus the form for immediate interaction
242 selectOption.Focus()
243
244 return &permissionDialogCmp{
245 permission: permission,
246 form: form,
247 selectOption: selectOption,
248 }
249}
250
251// NewPermissionDialogCmd creates a new permission dialog command
252func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
253 permDialog := newPermissionDialogCmp(permission)
254
255 // Create the dialog layout
256 dialogPane := layout.NewSinglePane(
257 permDialog.(*permissionDialogCmp),
258 layout.WithSinglePaneBordered(true),
259 layout.WithSinglePaneFocusable(true),
260 layout.WithSinglePaneActiveColor(styles.Warning),
261 layout.WithSignlePaneBorderText(map[layout.BorderPosition]string{
262 layout.TopMiddleBorder: " Permission Required ",
263 }),
264 )
265
266 // Focus the dialog
267 dialogPane.Focus()
268 widthRatio := 0.7
269 heightRatio := 0.6
270 minWidth := 100
271 minHeight := 30
272
273 // Make the dialog size more appropriate for bash commands
274 switch permission.ToolName {
275 case tools.BashToolName:
276 widthRatio = 0.7
277 heightRatio = 0.5
278 minWidth = 100
279 minHeight = 30
280 }
281 // Return the dialog command
282 return util.CmdHandler(core.DialogMsg{
283 Content: dialogPane,
284 WidthRatio: widthRatio,
285 HeightRatio: heightRatio,
286 MinWidth: minWidth,
287 MinHeight: minHeight,
288 })
289}