@@ -1,507 +0,0 @@
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"
19−type PermissionAction string
21−// Permission responses
23− PermissionAllow PermissionAction = "allow"
24− PermissionAllowForSession PermissionAction = "allow_session"
25− PermissionDeny PermissionAction = "deny"
28−// PermissionResponseMsg represents the user's response to a permission request
29−type PermissionResponseMsg struct {
30− Permission permission.PermissionRequest
31− Action PermissionAction
34−// PermissionDialogCmp interface for permission dialog component
35−type PermissionDialogCmp interface {
38− SetPermissions(permission permission.PermissionRequest) tea.Cmd
41−type permissionsMapping struct {
44− EnterSpace key.Binding
46− AllowSession key.Binding
51−var permissionsKeys = permissionsMapping{
52− Left: key.NewBinding(
53− key.WithKeys("left"),
54− key.WithHelp("←", "switch options"),
56− Right: key.NewBinding(
57− key.WithKeys("right"),
58− key.WithHelp("→", "switch options"),
60− EnterSpace: key.NewBinding(
61− key.WithKeys("enter", " "),
62− key.WithHelp("enter/space", "confirm"),
64− Allow: key.NewBinding(
66− key.WithHelp("a", "allow"),
68− AllowSession: key.NewBinding(
70− key.WithHelp("s", "allow for session"),
72− Deny: key.NewBinding(
74− key.WithHelp("d", "deny"),
78− key.WithHelp("tab", "switch options"),
82−// permissionDialogCmp is the implementation of PermissionDialog
83−type permissionDialogCmp struct {
86− permission permission.PermissionRequest
87− windowSize tea.WindowSizeMsg
88− contentViewPort viewport.Model
89− selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
91− diffCache map[string]string
92− markdownCache map[string]string
95−func (p *permissionDialogCmp) Init() tea.Cmd {
96− return p.contentViewPort.Init()
99−func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
102− switch msg := msg.(type) {
103− case tea.WindowSizeMsg:
106− cmds = append(cmds, cmd)
107− p.markdownCache = make(map[string]string)
108− p.diffCache = make(map[string]string)
109− case tea.KeyPressMsg:
111− case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
112− p.selectedOption = (p.selectedOption + 1) % 3
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})
125− // Pass other keys to viewport
126− viewPort, cmd := p.contentViewPort.Update(msg)
127− p.contentViewPort = viewPort
128− cmds = append(cmds, cmd)
132− return p, tea.Batch(cmds...)
135−func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
136− var action PermissionAction
138− switch p.selectedOption {
140− action = PermissionAllow
142− action = PermissionAllowForSession
144− action = PermissionDeny
147− return util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission})
150−func (p *permissionDialogCmp) renderButtons() string {
151− t := styles.CurrentTheme()
153− allowStyle := t.S().Text
154− allowSessionStyle := allowStyle
155− denyStyle := allowStyle
157− // Style the selected button
158− switch p.selectedOption {
160− allowStyle = allowStyle.Background(t.Secondary)
161− allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
162− denyStyle = denyStyle.Background(t.BgSubtle)
164− allowStyle = allowStyle.Background(t.BgSubtle)
165− allowSessionStyle = allowSessionStyle.Background(t.Secondary)
166− denyStyle = denyStyle.Background(t.BgSubtle)
168− allowStyle = allowStyle.Background(t.BgSubtle)
169− allowSessionStyle = allowSessionStyle.Background(t.BgSubtle)
170− denyStyle = denyStyle.Background(t.Secondary)
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)")
177− content := lipgloss.JoinHorizontal(
187− remainingWidth := p.width - lipgloss.Width(content)
188− if remainingWidth > 0 {
189− content = strings.Repeat(" ", remainingWidth) + content
194−func (p *permissionDialogCmp) renderHeader() string {
195− t := styles.CurrentTheme()
196− baseStyle := t.S().Base
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))
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))
208− headerParts := []string{
209− lipgloss.JoinHorizontal(
214− baseStyle.Render(strings.Repeat(" ", p.width)),
215− lipgloss.JoinHorizontal(
220− baseStyle.Render(strings.Repeat(" ", p.width)),
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(
239− baseStyle.Render(strings.Repeat(" ", p.width)),
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(
254− baseStyle.Render(strings.Repeat(" ", p.width)),
256− case tools.FetchToolName:
257− headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
260− return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
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)
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)
275− finalContent := baseStyle.
276− Width(p.contentViewPort.Width()).
277− Render(renderedContent)
278− p.contentViewPort.SetContent(finalContent)
279− return p.styleViewport()
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()))
290− p.contentViewPort.SetContent(diff)
291− return p.styleViewport()
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()))
302− p.contentViewPort.SetContent(diff)
303− return p.styleViewport()
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()))
315− p.contentViewPort.SetContent(diff)
316− return p.styleViewport()
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)
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)
333− finalContent := baseStyle.
334− Width(p.contentViewPort.Width()).
335− Render(renderedContent)
336− p.contentViewPort.SetContent(finalContent)
337− return p.styleViewport()
342−func (p *permissionDialogCmp) renderDefaultContent() string {
343− baseStyle := styles.CurrentTheme().S().Base
345− content := p.permission.Description
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)
354− finalContent := baseStyle.
355− Width(p.contentViewPort.Width()).
356− Render(renderedContent)
357− p.contentViewPort.SetContent(finalContent)
359− if renderedContent == "" {
363− return p.styleViewport()
366−func (p *permissionDialogCmp) styleViewport() string {
367− t := styles.CurrentTheme()
369− return t.S().Base.Render(p.contentViewPort.View())
372−func (p *permissionDialogCmp) render() string {
373− t := styles.CurrentTheme()
374− baseStyle := t.S().Base
379− Foreground(t.Primary).
380− Render("Permission Required")
382− headerContent := p.renderHeader()
384− buttons := p.renderButtons()
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)
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()
404− contentFinal = p.renderDefaultContent()
407− content := lipgloss.JoinVertical(
410− baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
414− baseStyle.Render(strings.Repeat(" ", p.width-4)),
418− Padding(1, 0, 0, 1).
419− Border(lipgloss.RoundedBorder()).
420− BorderForeground(t.BorderFocus).
428−func (p *permissionDialogCmp) View() tea.View {
429− return tea.NewView(p.render())
432−func (p *permissionDialogCmp) BindingKeys() []key.Binding {
433− return layout.KeyMapToSlice(permissionsKeys)
436−func (p *permissionDialogCmp) SetSize() tea.Cmd {
437− if p.permission.ID == "" {
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)
454− p.width = int(float64(p.windowSize.Width) * 0.7)
455− p.height = int(float64(p.windowSize.Height) * 0.5)
460−func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
461− p.permission = permission
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 {
471− content, err := generator()
473− return fmt.Sprintf("Error formatting diff: %v", err)
476− c.diffCache[key] = content
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 {
487− content, err := generator()
489− return fmt.Sprintf("Error rendering markdown: %v", err)
492− c.markdownCache[key] = content
497−func NewPermissionDialogCmp() PermissionDialogCmp {
498− // Create viewport for content
499− contentViewport := viewport.New()
501− return &permissionDialogCmp{
502− contentViewPort: contentViewport,
503− selectedOption: 0, // Default to "Allow"
504− diffCache: make(map[string]string),
505− markdownCache: make(map[string]string),