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