@@ -1,507 +0,0 @@
1−package dialog
2−
3−import (
4− "fmt"
5− "strings"
6−
7− "github.com/charmbracelet/bubbles/v2/key"
8− "github.com/charmbracelet/bubbles/v2/viewport"
9− tea "github.com/charmbracelet/bubbletea/v2"
10− "github.com/charmbracelet/crush/internal/diff"
11− "github.com/charmbracelet/crush/internal/llm/tools"
12− "github.com/charmbracelet/crush/internal/permission"
13− "github.com/charmbracelet/crush/internal/tui/layout"
14− "github.com/charmbracelet/crush/internal/tui/styles"
15− "github.com/charmbracelet/crush/internal/tui/util"
16− "github.com/charmbracelet/lipgloss/v2"
17−)
18−
19−type PermissionAction string
20−
21−// Permission responses
22−const (
23− PermissionAllow PermissionAction = "allow"
24− PermissionAllowForSession PermissionAction = "allow_session"
25− PermissionDeny PermissionAction = "deny"
26−)
27−
28−// PermissionResponseMsg represents the user's response to a permission request
29−type PermissionResponseMsg struct {
30− Permission permission.PermissionRequest
31− Action PermissionAction
32−}
33−
34−// PermissionDialogCmp interface for permission dialog component
35−type PermissionDialogCmp interface {
36− util.Model
37− layout.Bindings
38− SetPermissions(permission permission.PermissionRequest) tea.Cmd
39−}
40−
41−type permissionsMapping struct {
42− Left key.Binding
43− Right key.Binding
44− EnterSpace key.Binding
45− Allow key.Binding
46− AllowSession key.Binding
47− Deny key.Binding
48− Tab key.Binding
49−}
50−
51−var permissionsKeys = permissionsMapping{
52− Left: key.NewBinding(
53− key.WithKeys("left"),
54− key.WithHelp("←", "switch options"),
55− ),
56− Right: key.NewBinding(
57− key.WithKeys("right"),
58− key.WithHelp("→", "switch options"),
59− ),
60− EnterSpace: key.NewBinding(
61− key.WithKeys("enter", " "),
62− key.WithHelp("enter/space", "confirm"),
63− ),
64− Allow: key.NewBinding(
65− key.WithKeys("a"),
66− key.WithHelp("a", "allow"),
67− ),
68− AllowSession: key.NewBinding(
69− key.WithKeys("s"),
70− key.WithHelp("s", "allow for session"),
71− ),
72− Deny: key.NewBinding(
73− key.WithKeys("d"),
74− key.WithHelp("d", "deny"),
75− ),
76− Tab: key.NewBinding(
77− key.WithKeys("tab"),
78− key.WithHelp("tab", "switch options"),
79− ),
80−}
81−
82−// permissionDialogCmp is the implementation of PermissionDialog
83−type permissionDialogCmp struct {
84− width int
85− height int
86− permission permission.PermissionRequest
87− windowSize tea.WindowSizeMsg
88− contentViewPort viewport.Model
89− selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
90−
91− diffCache map[string]string
92− markdownCache map[string]string
93−}
94−
95−func (p *permissionDialogCmp) Init() tea.Cmd {
96− return p.contentViewPort.Init()
97−}
98−
99−func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
100− var cmds []tea.Cmd
101−
102− switch msg := msg.(type) {
103− case tea.WindowSizeMsg:
104− p.windowSize = msg
105− cmd := p.SetSize()
106− cmds = append(cmds, cmd)
107− p.markdownCache = make(map[string]string)
108− p.diffCache = make(map[string]string)
109− case tea.KeyPressMsg:
110− switch {
111− case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
112− p.selectedOption = (p.selectedOption + 1) % 3
113− return p, nil
114− case key.Matches(msg, permissionsKeys.Left):
115− p.selectedOption = (p.selectedOption + 2) % 3
116− case key.Matches(msg, permissionsKeys.EnterSpace):
117− return p, p.selectCurrentOption()
118− case key.Matches(msg, permissionsKeys.Allow):
119− return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
120− case key.Matches(msg, permissionsKeys.AllowSession):
121− return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
122− case key.Matches(msg, permissionsKeys.Deny):
123− return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
124− default:
125− // Pass other keys to viewport
126− viewPort, cmd := p.contentViewPort.Update(msg)
127− p.contentViewPort = viewPort
128− cmds = append(cmds, cmd)
129− }
130− }
131−
132− return p, tea.Batch(cmds...)
133−}
134−
135−func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
136− var action PermissionAction
137−
138− switch p.selectedOption {
139− case 0:
140− action = PermissionAllow
141− case 1:
142− action = PermissionAllowForSession
143− case 2:
144− action = PermissionDeny
145− }
146−
147− return util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission})
148−}
149−
150−func (p *permissionDialogCmp) renderButtons() string {
151− t := styles.CurrentTheme()
152−
153− allowStyle := t.S().Text
154− allowSessionStyle := allowStyle
155− denyStyle := allowStyle
156−
157− // Style the selected button
158− switch p.selectedOption {
159− case 0:
160− allowStyle = allowStyle.Background(t.Secondary)
161− allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
162− denyStyle = denyStyle.Background(t.BgSubtle)
163− case 1:
164− allowStyle = allowStyle.Background(t.BgSubtle)
165− allowSessionStyle = allowSessionStyle.Background(t.Secondary)
166− denyStyle = denyStyle.Background(t.BgSubtle)
167− case 2:
168− allowStyle = allowStyle.Background(t.BgSubtle)
169− allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
170− denyStyle = denyStyle.Background(t.Secondary)
171− }
172−
173− allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
174− allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
175− denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
176−
177− content := lipgloss.JoinHorizontal(
178− lipgloss.Left,
179− allowButton,
180− " ",
181− allowSessionButton,
182− " ",
183− denyButton,
184− " ",
185− )
186−
187− remainingWidth := p.width - lipgloss.Width(content)
188− if remainingWidth > 0 {
189− content = strings.Repeat(" ", remainingWidth) + content
190− }
191− return content
192−}
193−
194−func (p *permissionDialogCmp) renderHeader() string {
195− t := styles.CurrentTheme()
196− baseStyle := t.S().Base
197−
198− toolKey := t.S().Muted.Bold(true).Render("Tool")
199− toolValue := t.S().Text.
200− Width(p.width - lipgloss.Width(toolKey)).
201− Render(fmt.Sprintf(": %s", p.permission.ToolName))
202−
203− pathKey := t.S().Muted.Bold(true).Render("Path")
204− pathValue := t.S().Text.
205− Width(p.width - lipgloss.Width(pathKey)).
206− Render(fmt.Sprintf(": %s", p.permission.Path))
207−
208− headerParts := []string{
209− lipgloss.JoinHorizontal(
210− lipgloss.Left,
211− toolKey,
212− toolValue,
213− ),
214− baseStyle.Render(strings.Repeat(" ", p.width)),
215− lipgloss.JoinHorizontal(
216− lipgloss.Left,
217− pathKey,
218− pathValue,
219− ),
220− baseStyle.Render(strings.Repeat(" ", p.width)),
221− }
222−
223− // Add tool-specific header information
224− switch p.permission.ToolName {
225− case tools.BashToolName:
226− headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("Command"))
227− case tools.EditToolName:
228− params := p.permission.Params.(tools.EditPermissionsParams)
229− fileKey := t.S().Muted.Bold(true).Render("File")
230− filePath := t.S().Text.
231− Width(p.width - lipgloss.Width(fileKey)).
232− Render(fmt.Sprintf(": %s", params.FilePath))
233− headerParts = append(headerParts,
234− lipgloss.JoinHorizontal(
235− lipgloss.Left,
236− fileKey,
237− filePath,
238− ),
239− baseStyle.Render(strings.Repeat(" ", p.width)),
240− )
241−
242− case tools.WriteToolName:
243− params := p.permission.Params.(tools.WritePermissionsParams)
244− fileKey := t.S().Muted.Bold(true).Render("File")
245− filePath := t.S().Text.
246− Width(p.width - lipgloss.Width(fileKey)).
247− Render(fmt.Sprintf(": %s", params.FilePath))
248− headerParts = append(headerParts,
249− lipgloss.JoinHorizontal(
250− lipgloss.Left,
251− fileKey,
252− filePath,
253− ),
254− baseStyle.Render(strings.Repeat(" ", p.width)),
255− )
256− case tools.FetchToolName:
257− headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
258− }
259−
260− return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
261−}
262−
263−func (p *permissionDialogCmp) renderBashContent() string {
264− baseStyle := styles.CurrentTheme().S().Base
265− if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
266− content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
267−
268− // Use the cache for markdown rendering
269− renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
270− r := styles.GetMarkdownRenderer(p.width - 10)
271− s, err := r.Render(content)
272− return s, err
273− })
274−
275− finalContent := baseStyle.
276− Width(p.contentViewPort.Width()).
277− Render(renderedContent)
278− p.contentViewPort.SetContent(finalContent)
279− return p.styleViewport()
280− }
281− return ""
282−}
283−
284−func (p *permissionDialogCmp) renderEditContent() string {
285− if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
286− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
287− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
288− })
289−
290− p.contentViewPort.SetContent(diff)
291− return p.styleViewport()
292− }
293− return ""
294−}
295−
296−func (p *permissionDialogCmp) renderPatchContent() string {
297− if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
298− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
299− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
300− })
301−
302− p.contentViewPort.SetContent(diff)
303− return p.styleViewport()
304− }
305− return ""
306−}
307−
308−func (p *permissionDialogCmp) renderWriteContent() string {
309− if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
310− // Use the cache for diff rendering
311− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
312− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
313− })
314−
315− p.contentViewPort.SetContent(diff)
316− return p.styleViewport()
317− }
318− return ""
319−}
320−
321−func (p *permissionDialogCmp) renderFetchContent() string {
322− baseStyle := styles.CurrentTheme().S().Base
323− if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
324− content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
325−
326− // Use the cache for markdown rendering
327− renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
328− r := styles.GetMarkdownRenderer(p.width - 10)
329− s, err := r.Render(content)
330− return s, err
331− })
332−
333− finalContent := baseStyle.
334− Width(p.contentViewPort.Width()).
335− Render(renderedContent)
336− p.contentViewPort.SetContent(finalContent)
337− return p.styleViewport()
338− }
339− return ""
340−}
341−
342−func (p *permissionDialogCmp) renderDefaultContent() string {
343− baseStyle := styles.CurrentTheme().S().Base
344−
345− content := p.permission.Description
346−
347− // Use the cache for markdown rendering
348− renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
349− r := styles.GetMarkdownRenderer(p.width - 10)
350− s, err := r.Render(content)
351− return s, err
352− })
353−
354− finalContent := baseStyle.
355− Width(p.contentViewPort.Width()).
356− Render(renderedContent)
357− p.contentViewPort.SetContent(finalContent)
358−
359− if renderedContent == "" {
360− return ""
361− }
362−
363− return p.styleViewport()
364−}
365−
366−func (p *permissionDialogCmp) styleViewport() string {
367− t := styles.CurrentTheme()
368−
369− return t.S().Base.Render(p.contentViewPort.View())
370−}
371−
372−func (p *permissionDialogCmp) render() string {
373− t := styles.CurrentTheme()
374− baseStyle := t.S().Base
375−
376− title := baseStyle.
377− Bold(true).
378− Width(p.width - 4).
379− Foreground(t.Primary).
380− Render("Permission Required")
381− // Render header
382− headerContent := p.renderHeader()
383− // Render buttons
384− buttons := p.renderButtons()
385−
386− // Calculate content height dynamically based on window size
387− p.contentViewPort.SetHeight(p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title))
388− p.contentViewPort.SetWidth(p.width - 4)
389−
390− // Render content based on tool type
391− var contentFinal string
392− switch p.permission.ToolName {
393− case tools.BashToolName:
394− contentFinal = p.renderBashContent()
395− case tools.EditToolName:
396− contentFinal = p.renderEditContent()
397− case tools.PatchToolName:
398− contentFinal = p.renderPatchContent()
399− case tools.WriteToolName:
400− contentFinal = p.renderWriteContent()
401− case tools.FetchToolName:
402− contentFinal = p.renderFetchContent()
403− default:
404− contentFinal = p.renderDefaultContent()
405− }
406−
407− content := lipgloss.JoinVertical(
408− lipgloss.Top,
409− title,
410− baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
411− headerContent,
412− contentFinal,
413− buttons,
414− baseStyle.Render(strings.Repeat(" ", p.width-4)),
415− )
416−
417− return baseStyle.
418− Padding(1, 0, 0, 1).
419− Border(lipgloss.RoundedBorder()).
420− BorderForeground(t.BorderFocus).
421− Width(p.width).
422− Height(p.height).
423− Render(
424− content,
425− )
426−}
427−
428−func (p *permissionDialogCmp) View() tea.View {
429− return tea.NewView(p.render())
430−}
431−
432−func (p *permissionDialogCmp) BindingKeys() []key.Binding {
433− return layout.KeyMapToSlice(permissionsKeys)
434−}
435−
436−func (p *permissionDialogCmp) SetSize() tea.Cmd {
437− if p.permission.ID == "" {
438− return nil
439− }
440− switch p.permission.ToolName {
441− case tools.BashToolName:
442− p.width = int(float64(p.windowSize.Width) * 0.4)
443− p.height = int(float64(p.windowSize.Height) * 0.3)
444− case tools.EditToolName:
445− p.width = int(float64(p.windowSize.Width) * 0.8)
446− p.height = int(float64(p.windowSize.Height) * 0.8)
447− case tools.WriteToolName:
448− p.width = int(float64(p.windowSize.Width) * 0.8)
449− p.height = int(float64(p.windowSize.Height) * 0.8)
450− case tools.FetchToolName:
451− p.width = int(float64(p.windowSize.Width) * 0.4)
452− p.height = int(float64(p.windowSize.Height) * 0.3)
453− default:
454− p.width = int(float64(p.windowSize.Width) * 0.7)
455− p.height = int(float64(p.windowSize.Height) * 0.5)
456− }
457− return nil
458−}
459−
460−func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
461− p.permission = permission
462− return p.SetSize()
463−}
464−
465−// Helper to get or set cached diff content
466−func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
467− if cached, ok := c.diffCache[key]; ok {
468− return cached
469− }
470−
471− content, err := generator()
472− if err != nil {
473− return fmt.Sprintf("Error formatting diff: %v", err)
474− }
475−
476− c.diffCache[key] = content
477−
478− return content
479−}
480−
481−// Helper to get or set cached markdown content
482−func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
483− if cached, ok := c.markdownCache[key]; ok {
484− return cached
485− }
486−
487− content, err := generator()
488− if err != nil {
489− return fmt.Sprintf("Error rendering markdown: %v", err)
490− }
491−
492− c.markdownCache[key] = content
493−
494− return content
495−}
496−
497−func NewPermissionDialogCmp() PermissionDialogCmp {
498− // Create viewport for content
499− contentViewport := viewport.New()
500−
501− return &permissionDialogCmp{
502− contentViewPort: contentViewport,
503− selectedOption: 0, // Default to "Allow"
504− diffCache: make(map[string]string),
505− markdownCache: make(map[string]string),
506− }
507−}