permissions.go

  1package permissions
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/bubbles/v2/help"
  8	"github.com/charmbracelet/bubbles/v2/key"
  9	"github.com/charmbracelet/bubbles/v2/viewport"
 10	tea "github.com/charmbracelet/bubbletea/v2"
 11	"github.com/charmbracelet/crush/internal/fsext"
 12	"github.com/charmbracelet/crush/internal/llm/tools"
 13	"github.com/charmbracelet/crush/internal/permission"
 14	"github.com/charmbracelet/crush/internal/tui/components/core"
 15	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 16	"github.com/charmbracelet/crush/internal/tui/styles"
 17	"github.com/charmbracelet/crush/internal/tui/util"
 18	"github.com/charmbracelet/lipgloss/v2"
 19	"github.com/charmbracelet/x/ansi"
 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	PermissionsDialogID dialogs.DialogID = "permissions"
 31)
 32
 33// PermissionResponseMsg represents the user's response to a permission request
 34type PermissionResponseMsg struct {
 35	Permission permission.PermissionRequest
 36	Action     PermissionAction
 37}
 38
 39// PermissionDialogCmp interface for permission dialog component
 40type PermissionDialogCmp interface {
 41	dialogs.DialogModel
 42}
 43
 44// permissionDialogCmp is the implementation of PermissionDialog
 45type permissionDialogCmp struct {
 46	wWidth          int
 47	wHeight         int
 48	width           int
 49	height          int
 50	permission      permission.PermissionRequest
 51	contentViewPort viewport.Model
 52	selectedOption  int // 0: Allow, 1: Allow for session, 2: Deny
 53
 54	// Diff view state
 55	defaultDiffSplitMode bool  // true for split, false for unified
 56	diffSplitMode        *bool // nil means use defaultDiffSplitMode
 57	diffXOffset          int   // horizontal scroll offset
 58	diffYOffset          int   // vertical scroll offset
 59
 60	// Caching
 61	cachedContent string
 62	contentDirty  bool
 63
 64	positionRow int // Row position for dialog
 65	positionCol int // Column position for dialog
 66
 67	finalDialogHeight int
 68
 69	keyMap KeyMap
 70}
 71
 72func NewPermissionDialogCmp(permission permission.PermissionRequest, opts *Options) PermissionDialogCmp {
 73	if opts == nil {
 74		opts = &Options{}
 75	}
 76
 77	// Create viewport for content
 78	contentViewport := viewport.New()
 79	return &permissionDialogCmp{
 80		contentViewPort: contentViewport,
 81		selectedOption:  0, // Default to "Allow"
 82		permission:      permission,
 83		diffSplitMode:   opts.isSplitMode(),
 84		keyMap:          DefaultKeyMap(),
 85		contentDirty:    true, // Mark as dirty initially
 86	}
 87}
 88
 89func (p *permissionDialogCmp) Init() tea.Cmd {
 90	return p.contentViewPort.Init()
 91}
 92
 93func (p *permissionDialogCmp) supportsDiffView() bool {
 94	return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName
 95}
 96
 97func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 98	var cmds []tea.Cmd
 99
100	switch msg := msg.(type) {
101	case tea.WindowSizeMsg:
102		p.wWidth = msg.Width
103		p.wHeight = msg.Height
104		p.contentDirty = true // Mark content as dirty on window resize
105		cmd := p.SetSize()
106		cmds = append(cmds, cmd)
107	case tea.KeyPressMsg:
108		switch {
109		case key.Matches(msg, p.keyMap.Right) || key.Matches(msg, p.keyMap.Tab):
110			p.selectedOption = (p.selectedOption + 1) % 3
111			return p, nil
112		case key.Matches(msg, p.keyMap.Left):
113			p.selectedOption = (p.selectedOption + 2) % 3
114		case key.Matches(msg, p.keyMap.Select):
115			return p, p.selectCurrentOption()
116		case key.Matches(msg, p.keyMap.Allow):
117			return p, tea.Batch(
118				util.CmdHandler(dialogs.CloseDialogMsg{}),
119				util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission}),
120			)
121		case key.Matches(msg, p.keyMap.AllowSession):
122			return p, tea.Batch(
123				util.CmdHandler(dialogs.CloseDialogMsg{}),
124				util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission}),
125			)
126		case key.Matches(msg, p.keyMap.Deny):
127			return p, tea.Batch(
128				util.CmdHandler(dialogs.CloseDialogMsg{}),
129				util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}),
130			)
131		case key.Matches(msg, p.keyMap.ToggleDiffMode):
132			if p.supportsDiffView() {
133				if p.diffSplitMode == nil {
134					diffSplitMode := !p.defaultDiffSplitMode
135					p.diffSplitMode = &diffSplitMode
136				} else {
137					*p.diffSplitMode = !*p.diffSplitMode
138				}
139				p.contentDirty = true // Mark content as dirty when diff mode changes
140				return p, nil
141			}
142		case key.Matches(msg, p.keyMap.ScrollDown):
143			if p.supportsDiffView() {
144				p.scrollDown()
145				return p, nil
146			}
147		case key.Matches(msg, p.keyMap.ScrollUp):
148			if p.supportsDiffView() {
149				p.scrollUp()
150				return p, nil
151			}
152		case key.Matches(msg, p.keyMap.ScrollLeft):
153			if p.supportsDiffView() {
154				p.scrollLeft()
155				return p, nil
156			}
157		case key.Matches(msg, p.keyMap.ScrollRight):
158			if p.supportsDiffView() {
159				p.scrollRight()
160				return p, nil
161			}
162		default:
163			// Pass other keys to viewport
164			viewPort, cmd := p.contentViewPort.Update(msg)
165			p.contentViewPort = viewPort
166			cmds = append(cmds, cmd)
167		}
168	case tea.MouseWheelMsg:
169		if p.supportsDiffView() && p.isMouseOverDialog(msg.Mouse().X, msg.Mouse().Y) {
170			switch msg.Button {
171			case tea.MouseWheelDown:
172				p.scrollDown()
173			case tea.MouseWheelUp:
174				p.scrollUp()
175			case tea.MouseWheelLeft:
176				p.scrollLeft()
177			case tea.MouseWheelRight:
178				p.scrollRight()
179			}
180		}
181	}
182
183	return p, tea.Batch(cmds...)
184}
185
186func (p *permissionDialogCmp) scrollDown() {
187	p.diffYOffset += 1
188	p.contentDirty = true
189}
190
191func (p *permissionDialogCmp) scrollUp() {
192	p.diffYOffset = max(0, p.diffYOffset-1)
193	p.contentDirty = true
194}
195
196func (p *permissionDialogCmp) scrollLeft() {
197	p.diffXOffset = max(0, p.diffXOffset-5)
198	p.contentDirty = true
199}
200
201func (p *permissionDialogCmp) scrollRight() {
202	p.diffXOffset += 5
203	p.contentDirty = true
204}
205
206// isMouseOverDialog checks if the given mouse coordinates are within the dialog bounds.
207// Returns true if the mouse is over the dialog area, false otherwise.
208func (p *permissionDialogCmp) isMouseOverDialog(x, y int) bool {
209	if p.permission.ID == "" {
210		return false
211	}
212	var (
213		dialogX      = p.positionCol
214		dialogY      = p.positionRow
215		dialogWidth  = p.width
216		dialogHeight = p.finalDialogHeight
217	)
218	return x >= dialogX && x < dialogX+dialogWidth && y >= dialogY && y < dialogY+dialogHeight
219}
220
221func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
222	var action PermissionAction
223
224	switch p.selectedOption {
225	case 0:
226		action = PermissionAllow
227	case 1:
228		action = PermissionAllowForSession
229	case 2:
230		action = PermissionDeny
231	}
232
233	return tea.Batch(
234		util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission}),
235		util.CmdHandler(dialogs.CloseDialogMsg{}),
236	)
237}
238
239func (p *permissionDialogCmp) renderButtons() string {
240	t := styles.CurrentTheme()
241	baseStyle := t.S().Base
242
243	buttons := []core.ButtonOpts{
244		{
245			Text:           "Allow",
246			UnderlineIndex: 0, // "A"
247			Selected:       p.selectedOption == 0,
248		},
249		{
250			Text:           "Allow for Session",
251			UnderlineIndex: 10, // "S" in "Session"
252			Selected:       p.selectedOption == 1,
253		},
254		{
255			Text:           "Deny",
256			UnderlineIndex: 0, // "D"
257			Selected:       p.selectedOption == 2,
258		},
259	}
260
261	content := core.SelectableButtons(buttons, "  ")
262	if lipgloss.Width(content) > p.width-4 {
263		content = core.SelectableButtonsVertical(buttons, 1)
264		return baseStyle.AlignVertical(lipgloss.Center).
265			AlignHorizontal(lipgloss.Center).
266			Width(p.width - 4).
267			Render(content)
268	}
269
270	return baseStyle.AlignHorizontal(lipgloss.Right).Width(p.width - 4).Render(content)
271}
272
273func (p *permissionDialogCmp) renderHeader() string {
274	t := styles.CurrentTheme()
275	baseStyle := t.S().Base
276
277	toolKey := t.S().Muted.Render("Tool")
278	toolValue := t.S().Text.
279		Width(p.width - lipgloss.Width(toolKey)).
280		Render(fmt.Sprintf(" %s", p.permission.ToolName))
281
282	pathKey := t.S().Muted.Render("Path")
283	pathValue := t.S().Text.
284		Width(p.width - lipgloss.Width(pathKey)).
285		Render(fmt.Sprintf(" %s", fsext.PrettyPath(p.permission.Path)))
286
287	headerParts := []string{
288		lipgloss.JoinHorizontal(
289			lipgloss.Left,
290			toolKey,
291			toolValue,
292		),
293		baseStyle.Render(strings.Repeat(" ", p.width)),
294		lipgloss.JoinHorizontal(
295			lipgloss.Left,
296			pathKey,
297			pathValue,
298		),
299		baseStyle.Render(strings.Repeat(" ", p.width)),
300	}
301
302	// Add tool-specific header information
303	switch p.permission.ToolName {
304	case tools.BashToolName:
305		headerParts = append(headerParts, t.S().Muted.Width(p.width).Render("Command"))
306	case tools.DownloadToolName:
307		params := p.permission.Params.(tools.DownloadPermissionsParams)
308		urlKey := t.S().Muted.Render("URL")
309		urlValue := t.S().Text.
310			Width(p.width - lipgloss.Width(urlKey)).
311			Render(fmt.Sprintf(" %s", params.URL))
312		fileKey := t.S().Muted.Render("File")
313		filePath := t.S().Text.
314			Width(p.width - lipgloss.Width(fileKey)).
315			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
316		headerParts = append(headerParts,
317			lipgloss.JoinHorizontal(
318				lipgloss.Left,
319				urlKey,
320				urlValue,
321			),
322			baseStyle.Render(strings.Repeat(" ", p.width)),
323			lipgloss.JoinHorizontal(
324				lipgloss.Left,
325				fileKey,
326				filePath,
327			),
328			baseStyle.Render(strings.Repeat(" ", p.width)),
329		)
330	case tools.EditToolName:
331		params := p.permission.Params.(tools.EditPermissionsParams)
332		fileKey := t.S().Muted.Render("File")
333		filePath := t.S().Text.
334			Width(p.width - lipgloss.Width(fileKey)).
335			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
336		headerParts = append(headerParts,
337			lipgloss.JoinHorizontal(
338				lipgloss.Left,
339				fileKey,
340				filePath,
341			),
342			baseStyle.Render(strings.Repeat(" ", p.width)),
343		)
344
345	case tools.WriteToolName:
346		params := p.permission.Params.(tools.WritePermissionsParams)
347		fileKey := t.S().Muted.Render("File")
348		filePath := t.S().Text.
349			Width(p.width - lipgloss.Width(fileKey)).
350			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
351		headerParts = append(headerParts,
352			lipgloss.JoinHorizontal(
353				lipgloss.Left,
354				fileKey,
355				filePath,
356			),
357			baseStyle.Render(strings.Repeat(" ", p.width)),
358		)
359	case tools.MultiEditToolName:
360		params := p.permission.Params.(tools.MultiEditPermissionsParams)
361		fileKey := t.S().Muted.Render("File")
362		filePath := t.S().Text.
363			Width(p.width - lipgloss.Width(fileKey)).
364			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
365		headerParts = append(headerParts,
366			lipgloss.JoinHorizontal(
367				lipgloss.Left,
368				fileKey,
369				filePath,
370			),
371			baseStyle.Render(strings.Repeat(" ", p.width)),
372		)
373	case tools.FetchToolName:
374		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
375	case tools.ViewToolName:
376		params := p.permission.Params.(tools.ViewPermissionsParams)
377		fileKey := t.S().Muted.Render("File")
378		filePath := t.S().Text.
379			Width(p.width - lipgloss.Width(fileKey)).
380			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
381		headerParts = append(headerParts,
382			lipgloss.JoinHorizontal(
383				lipgloss.Left,
384				fileKey,
385				filePath,
386			),
387			baseStyle.Render(strings.Repeat(" ", p.width)),
388		)
389	case tools.LSToolName:
390		params := p.permission.Params.(tools.LSPermissionsParams)
391		pathKey := t.S().Muted.Render("Directory")
392		pathValue := t.S().Text.
393			Width(p.width - lipgloss.Width(pathKey)).
394			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.Path)))
395		headerParts = append(headerParts,
396			lipgloss.JoinHorizontal(
397				lipgloss.Left,
398				pathKey,
399				pathValue,
400			),
401			baseStyle.Render(strings.Repeat(" ", p.width)),
402		)
403	}
404
405	return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
406}
407
408func (p *permissionDialogCmp) getOrGenerateContent() string {
409	// Return cached content if available and not dirty
410	if !p.contentDirty && p.cachedContent != "" {
411		return p.cachedContent
412	}
413
414	// Generate new content
415	var content string
416	switch p.permission.ToolName {
417	case tools.BashToolName:
418		content = p.generateBashContent()
419	case tools.DownloadToolName:
420		content = p.generateDownloadContent()
421	case tools.EditToolName:
422		content = p.generateEditContent()
423	case tools.WriteToolName:
424		content = p.generateWriteContent()
425	case tools.MultiEditToolName:
426		content = p.generateMultiEditContent()
427	case tools.FetchToolName:
428		content = p.generateFetchContent()
429	case tools.ViewToolName:
430		content = p.generateViewContent()
431	case tools.LSToolName:
432		content = p.generateLSContent()
433	default:
434		content = p.generateDefaultContent()
435	}
436
437	// Cache the result
438	p.cachedContent = content
439	p.contentDirty = false
440
441	return content
442}
443
444func (p *permissionDialogCmp) generateBashContent() string {
445	t := styles.CurrentTheme()
446	baseStyle := t.S().Base.Background(t.BgSubtle)
447	if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
448		content := pr.Command
449		t := styles.CurrentTheme()
450		content = strings.TrimSpace(content)
451		lines := strings.Split(content, "\n")
452
453		width := p.width - 4
454		var out []string
455		for _, ln := range lines {
456			out = append(out, t.S().Muted.
457				Width(width).
458				Padding(0, 3).
459				Foreground(t.FgBase).
460				Background(t.BgSubtle).
461				Render(ln))
462		}
463
464		// Use the cache for markdown rendering
465		renderedContent := strings.Join(out, "\n")
466		finalContent := baseStyle.
467			Width(p.contentViewPort.Width()).
468			Padding(1, 0).
469			Render(renderedContent)
470
471		return finalContent
472	}
473	return ""
474}
475
476func (p *permissionDialogCmp) generateEditContent() string {
477	if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
478		formatter := core.DiffFormatter().
479			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
480			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
481			Height(p.contentViewPort.Height()).
482			Width(p.contentViewPort.Width()).
483			XOffset(p.diffXOffset).
484			YOffset(p.diffYOffset)
485		if p.useDiffSplitMode() {
486			formatter = formatter.Split()
487		} else {
488			formatter = formatter.Unified()
489		}
490
491		diff := formatter.String()
492		return diff
493	}
494	return ""
495}
496
497func (p *permissionDialogCmp) generateWriteContent() string {
498	if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
499		// Use the cache for diff rendering
500		formatter := core.DiffFormatter().
501			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
502			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
503			Height(p.contentViewPort.Height()).
504			Width(p.contentViewPort.Width()).
505			XOffset(p.diffXOffset).
506			YOffset(p.diffYOffset)
507		if p.useDiffSplitMode() {
508			formatter = formatter.Split()
509		} else {
510			formatter = formatter.Unified()
511		}
512
513		diff := formatter.String()
514		return diff
515	}
516	return ""
517}
518
519func (p *permissionDialogCmp) generateDownloadContent() string {
520	t := styles.CurrentTheme()
521	baseStyle := t.S().Base.Background(t.BgSubtle)
522	if pr, ok := p.permission.Params.(tools.DownloadPermissionsParams); ok {
523		content := fmt.Sprintf("URL: %s\nFile: %s", pr.URL, fsext.PrettyPath(pr.FilePath))
524		if pr.Timeout > 0 {
525			content += fmt.Sprintf("\nTimeout: %ds", pr.Timeout)
526		}
527
528		finalContent := baseStyle.
529			Padding(1, 2).
530			Width(p.contentViewPort.Width()).
531			Render(content)
532		return finalContent
533	}
534	return ""
535}
536
537func (p *permissionDialogCmp) generateMultiEditContent() string {
538	if pr, ok := p.permission.Params.(tools.MultiEditPermissionsParams); ok {
539		// Use the cache for diff rendering
540		formatter := core.DiffFormatter().
541			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
542			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
543			Height(p.contentViewPort.Height()).
544			Width(p.contentViewPort.Width()).
545			XOffset(p.diffXOffset).
546			YOffset(p.diffYOffset)
547		if p.useDiffSplitMode() {
548			formatter = formatter.Split()
549		} else {
550			formatter = formatter.Unified()
551		}
552
553		diff := formatter.String()
554		return diff
555	}
556	return ""
557}
558
559func (p *permissionDialogCmp) generateFetchContent() string {
560	t := styles.CurrentTheme()
561	baseStyle := t.S().Base.Background(t.BgSubtle)
562	if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
563		finalContent := baseStyle.
564			Padding(1, 2).
565			Width(p.contentViewPort.Width()).
566			Render(pr.URL)
567		return finalContent
568	}
569	return ""
570}
571
572func (p *permissionDialogCmp) generateViewContent() string {
573	t := styles.CurrentTheme()
574	baseStyle := t.S().Base.Background(t.BgSubtle)
575	if pr, ok := p.permission.Params.(tools.ViewPermissionsParams); ok {
576		content := fmt.Sprintf("File: %s", fsext.PrettyPath(pr.FilePath))
577		if pr.Offset > 0 {
578			content += fmt.Sprintf("\nStarting from line: %d", pr.Offset+1)
579		}
580		if pr.Limit > 0 && pr.Limit != 2000 { // 2000 is the default limit
581			content += fmt.Sprintf("\nLines to read: %d", pr.Limit)
582		}
583
584		finalContent := baseStyle.
585			Padding(1, 2).
586			Width(p.contentViewPort.Width()).
587			Render(content)
588		return finalContent
589	}
590	return ""
591}
592
593func (p *permissionDialogCmp) generateLSContent() string {
594	t := styles.CurrentTheme()
595	baseStyle := t.S().Base.Background(t.BgSubtle)
596	if pr, ok := p.permission.Params.(tools.LSPermissionsParams); ok {
597		content := fmt.Sprintf("Directory: %s", fsext.PrettyPath(pr.Path))
598		if len(pr.Ignore) > 0 {
599			content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(pr.Ignore, ", "))
600		}
601
602		finalContent := baseStyle.
603			Padding(1, 2).
604			Width(p.contentViewPort.Width()).
605			Render(content)
606		return finalContent
607	}
608	return ""
609}
610
611func (p *permissionDialogCmp) generateDefaultContent() string {
612	t := styles.CurrentTheme()
613	baseStyle := t.S().Base.Background(t.BgSubtle)
614
615	content := p.permission.Description
616
617	content = strings.TrimSpace(content)
618	content = "\n" + content + "\n"
619	lines := strings.Split(content, "\n")
620
621	width := p.width - 4
622	var out []string
623	for _, ln := range lines {
624		ln = " " + ln // left padding
625		if len(ln) > width {
626			ln = ansi.Truncate(ln, width, "…")
627		}
628		out = append(out, t.S().Muted.
629			Width(width).
630			Foreground(t.FgBase).
631			Background(t.BgSubtle).
632			Render(ln))
633	}
634
635	// Use the cache for markdown rendering
636	renderedContent := strings.Join(out, "\n")
637	finalContent := baseStyle.
638		Width(p.contentViewPort.Width()).
639		Render(renderedContent)
640
641	if renderedContent == "" {
642		return ""
643	}
644
645	return finalContent
646}
647
648func (p *permissionDialogCmp) useDiffSplitMode() bool {
649	if p.diffSplitMode != nil {
650		return *p.diffSplitMode
651	}
652	return p.defaultDiffSplitMode
653}
654
655func (p *permissionDialogCmp) styleViewport() string {
656	t := styles.CurrentTheme()
657	return t.S().Base.Render(p.contentViewPort.View())
658}
659
660func (p *permissionDialogCmp) render() string {
661	t := styles.CurrentTheme()
662	baseStyle := t.S().Base
663	title := core.Title("Permission Required", p.width-4)
664	// Render header
665	headerContent := p.renderHeader()
666	// Render buttons
667	buttons := p.renderButtons()
668
669	p.contentViewPort.SetWidth(p.width - 4)
670
671	// Get cached or generate content
672	contentFinal := p.getOrGenerateContent()
673
674	// Always set viewport content (the caching is handled in getOrGenerateContent)
675	const minContentHeight = 9
676	contentHeight := min(
677		max(minContentHeight, p.height-minContentHeight),
678		lipgloss.Height(contentFinal),
679	)
680	p.contentViewPort.SetHeight(contentHeight)
681	p.contentViewPort.SetContent(contentFinal)
682
683	p.positionRow = p.wHeight / 2
684	p.positionRow -= (contentHeight + 9) / 2
685	p.positionRow -= 3 // Move dialog slightly higher than middle
686
687	var contentHelp string
688	if p.supportsDiffView() {
689		contentHelp = help.New().View(p.keyMap)
690	}
691
692	// Calculate content height dynamically based on window size
693	strs := []string{
694		title,
695		"",
696		headerContent,
697		p.styleViewport(),
698		"",
699		buttons,
700		"",
701	}
702	if contentHelp != "" {
703		strs = append(strs, "", contentHelp)
704	}
705	content := lipgloss.JoinVertical(lipgloss.Top, strs...)
706
707	dialog := baseStyle.
708		Padding(0, 1).
709		Border(lipgloss.RoundedBorder()).
710		BorderForeground(t.BorderFocus).
711		Width(p.width).
712		Render(
713			content,
714		)
715	p.finalDialogHeight = lipgloss.Height(dialog)
716	return dialog
717}
718
719func (p *permissionDialogCmp) View() string {
720	return p.render()
721}
722
723func (p *permissionDialogCmp) SetSize() tea.Cmd {
724	if p.permission.ID == "" {
725		return nil
726	}
727
728	oldWidth, oldHeight := p.width, p.height
729
730	switch p.permission.ToolName {
731	case tools.BashToolName:
732		p.width = int(float64(p.wWidth) * 0.8)
733		p.height = int(float64(p.wHeight) * 0.3)
734	case tools.DownloadToolName:
735		p.width = int(float64(p.wWidth) * 0.8)
736		p.height = int(float64(p.wHeight) * 0.4)
737	case tools.EditToolName:
738		p.width = int(float64(p.wWidth) * 0.8)
739		p.height = int(float64(p.wHeight) * 0.8)
740	case tools.WriteToolName:
741		p.width = int(float64(p.wWidth) * 0.8)
742		p.height = int(float64(p.wHeight) * 0.8)
743	case tools.MultiEditToolName:
744		p.width = int(float64(p.wWidth) * 0.8)
745		p.height = int(float64(p.wHeight) * 0.8)
746	case tools.FetchToolName:
747		p.width = int(float64(p.wWidth) * 0.8)
748		p.height = int(float64(p.wHeight) * 0.3)
749	case tools.ViewToolName:
750		p.width = int(float64(p.wWidth) * 0.8)
751		p.height = int(float64(p.wHeight) * 0.4)
752	case tools.LSToolName:
753		p.width = int(float64(p.wWidth) * 0.8)
754		p.height = int(float64(p.wHeight) * 0.4)
755	default:
756		p.width = int(float64(p.wWidth) * 0.7)
757		p.height = int(float64(p.wHeight) * 0.5)
758	}
759
760	// Default to diff split mode when dialog is wide enough.
761	p.defaultDiffSplitMode = p.width >= 140
762
763	// Set a maximum width for the dialog
764	p.width = min(p.width, 180)
765
766	// Mark content as dirty if size changed
767	if oldWidth != p.width || oldHeight != p.height {
768		p.contentDirty = true
769	}
770	p.positionRow = p.wHeight / 2
771	p.positionRow -= p.height / 2
772	p.positionRow -= 3 // Move dialog slightly higher than middle
773	p.positionCol = p.wWidth / 2
774	p.positionCol -= p.width / 2
775	return nil
776}
777
778func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
779	content, err := generator()
780	if err != nil {
781		return fmt.Sprintf("Error rendering markdown: %v", err)
782	}
783
784	return content
785}
786
787// ID implements PermissionDialogCmp.
788func (p *permissionDialogCmp) ID() dialogs.DialogID {
789	return PermissionsDialogID
790}
791
792// Position implements PermissionDialogCmp.
793func (p *permissionDialogCmp) Position() (int, int) {
794	return p.positionRow, p.positionCol
795}
796
797// Options for create a new permission dialog
798type Options struct {
799	DiffMode string // split or unified, empty means use defaultDiffSplitMode
800}
801
802// isSplitMode returns internal representation of diff mode switch
803func (o Options) isSplitMode() *bool {
804	var split bool
805
806	switch o.DiffMode {
807	case "split":
808		split = true
809	case "unified":
810		split = false
811	default:
812		return nil
813	}
814
815	return &split
816}