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