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