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