permissions.go

  1package permissions
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/charmbracelet/bubbles/v2/help"
  9	"github.com/charmbracelet/bubbles/v2/key"
 10	"github.com/charmbracelet/bubbles/v2/viewport"
 11	tea "github.com/charmbracelet/bubbletea/v2"
 12	"github.com/charmbracelet/crush/internal/agent/tools"
 13	"github.com/charmbracelet/crush/internal/fsext"
 14	"github.com/charmbracelet/crush/internal/permission"
 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 string
 24
 25// Permission responses
 26const (
 27	PermissionAllow           PermissionAction = "allow"
 28	PermissionAllowForSession PermissionAction = "allow_session"
 29	PermissionDeny            PermissionAction = "deny"
 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	// Add pretty-printed JSON parameters for MCP tools
619	if p.permission.Params != nil {
620		var paramStr string
621
622		// Ensure params is a string
623		if str, ok := p.permission.Params.(string); ok {
624			paramStr = str
625		} else {
626			paramStr = fmt.Sprintf("%v", p.permission.Params)
627		}
628
629		// Try to parse as JSON for pretty printing
630		var parsed any
631		if err := json.Unmarshal([]byte(paramStr), &parsed); err == nil {
632			if b, err := json.MarshalIndent(parsed, "", "  "); err == nil {
633				if content != "" {
634					content += "\n\n"
635				}
636				content += string(b)
637			}
638		} else {
639			// Not JSON, show as-is
640			if content != "" {
641				content += "\n\n"
642			}
643			content += paramStr
644		}
645	}
646
647	content = strings.TrimSpace(content)
648	content = "\n" + content + "\n"
649	lines := strings.Split(content, "\n")
650
651	width := p.width - 4
652	var out []string
653	for _, ln := range lines {
654		ln = " " + ln // left padding
655		if len(ln) > width {
656			ln = ansi.Truncate(ln, width, "…")
657		}
658		out = append(out, t.S().Muted.
659			Width(width).
660			Foreground(t.FgBase).
661			Background(t.BgSubtle).
662			Render(ln))
663	}
664
665	// Use the cache for markdown rendering
666	renderedContent := strings.Join(out, "\n")
667	finalContent := baseStyle.
668		Width(p.contentViewPort.Width()).
669		Render(renderedContent)
670
671	if renderedContent == "" {
672		return ""
673	}
674
675	return finalContent
676}
677
678func (p *permissionDialogCmp) useDiffSplitMode() bool {
679	if p.diffSplitMode != nil {
680		return *p.diffSplitMode
681	}
682	return p.defaultDiffSplitMode
683}
684
685func (p *permissionDialogCmp) styleViewport() string {
686	t := styles.CurrentTheme()
687	return t.S().Base.Render(p.contentViewPort.View())
688}
689
690func (p *permissionDialogCmp) render() string {
691	t := styles.CurrentTheme()
692	baseStyle := t.S().Base
693	title := core.Title("Permission Required", p.width-4)
694	// Render header
695	headerContent := p.renderHeader()
696	// Render buttons
697	buttons := p.renderButtons()
698
699	p.contentViewPort.SetWidth(p.width - 4)
700
701	// Get cached or generate content
702	contentFinal := p.getOrGenerateContent()
703
704	// Always set viewport content (the caching is handled in getOrGenerateContent)
705	const minContentHeight = 9
706	contentHeight := min(
707		max(minContentHeight, p.height-minContentHeight),
708		lipgloss.Height(contentFinal),
709	)
710	p.contentViewPort.SetHeight(contentHeight)
711	p.contentViewPort.SetContent(contentFinal)
712
713	p.positionRow = p.wHeight / 2
714	p.positionRow -= (contentHeight + 9) / 2
715	p.positionRow -= 3 // Move dialog slightly higher than middle
716
717	var contentHelp string
718	if p.supportsDiffView() {
719		contentHelp = help.New().View(p.keyMap)
720	}
721
722	// Calculate content height dynamically based on window size
723	strs := []string{
724		title,
725		"",
726		headerContent,
727		p.styleViewport(),
728		"",
729		buttons,
730		"",
731	}
732	if contentHelp != "" {
733		strs = append(strs, "", contentHelp)
734	}
735	content := lipgloss.JoinVertical(lipgloss.Top, strs...)
736
737	dialog := baseStyle.
738		Padding(0, 1).
739		Border(lipgloss.RoundedBorder()).
740		BorderForeground(t.BorderFocus).
741		Width(p.width).
742		Render(
743			content,
744		)
745	p.finalDialogHeight = lipgloss.Height(dialog)
746	return dialog
747}
748
749func (p *permissionDialogCmp) View() string {
750	return p.render()
751}
752
753func (p *permissionDialogCmp) SetSize() tea.Cmd {
754	if p.permission.ID == "" {
755		return nil
756	}
757
758	oldWidth, oldHeight := p.width, p.height
759
760	switch p.permission.ToolName {
761	case tools.BashToolName:
762		p.width = int(float64(p.wWidth) * 0.8)
763		p.height = int(float64(p.wHeight) * 0.3)
764	case tools.DownloadToolName:
765		p.width = int(float64(p.wWidth) * 0.8)
766		p.height = int(float64(p.wHeight) * 0.4)
767	case tools.EditToolName:
768		p.width = int(float64(p.wWidth) * 0.8)
769		p.height = int(float64(p.wHeight) * 0.8)
770	case tools.WriteToolName:
771		p.width = int(float64(p.wWidth) * 0.8)
772		p.height = int(float64(p.wHeight) * 0.8)
773	case tools.MultiEditToolName:
774		p.width = int(float64(p.wWidth) * 0.8)
775		p.height = int(float64(p.wHeight) * 0.8)
776	case tools.FetchToolName:
777		p.width = int(float64(p.wWidth) * 0.8)
778		p.height = int(float64(p.wHeight) * 0.3)
779	case tools.ViewToolName:
780		p.width = int(float64(p.wWidth) * 0.8)
781		p.height = int(float64(p.wHeight) * 0.4)
782	case tools.LSToolName:
783		p.width = int(float64(p.wWidth) * 0.8)
784		p.height = int(float64(p.wHeight) * 0.4)
785	default:
786		p.width = int(float64(p.wWidth) * 0.7)
787		p.height = int(float64(p.wHeight) * 0.5)
788	}
789
790	// Default to diff split mode when dialog is wide enough.
791	p.defaultDiffSplitMode = p.width >= 140
792
793	// Set a maximum width for the dialog
794	p.width = min(p.width, 180)
795
796	// Mark content as dirty if size changed
797	if oldWidth != p.width || oldHeight != p.height {
798		p.contentDirty = true
799	}
800	p.positionRow = p.wHeight / 2
801	p.positionRow -= p.height / 2
802	p.positionRow -= 3 // Move dialog slightly higher than middle
803	p.positionCol = p.wWidth / 2
804	p.positionCol -= p.width / 2
805	return nil
806}
807
808func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
809	content, err := generator()
810	if err != nil {
811		return fmt.Sprintf("Error rendering markdown: %v", err)
812	}
813
814	return content
815}
816
817// ID implements PermissionDialogCmp.
818func (p *permissionDialogCmp) ID() dialogs.DialogID {
819	return PermissionsDialogID
820}
821
822// Position implements PermissionDialogCmp.
823func (p *permissionDialogCmp) Position() (int, int) {
824	return p.positionRow, p.positionCol
825}
826
827// Options for create a new permission dialog
828type Options struct {
829	DiffMode string // split or unified, empty means use defaultDiffSplitMode
830}
831
832// isSplitMode returns internal representation of diff mode switch
833func (o Options) isSplitMode() *bool {
834	var split bool
835
836	switch o.DiffMode {
837	case "split":
838		split = true
839	case "unified":
840		split = false
841	default:
842		return nil
843	}
844
845	return &split
846}