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