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) (util.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.AgenticFetchToolName:
377		headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
378	case tools.ViewToolName:
379		params := p.permission.Params.(tools.ViewPermissionsParams)
380		fileKey := t.S().Muted.Render("File")
381		filePath := t.S().Text.
382			Width(p.width - lipgloss.Width(fileKey)).
383			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
384		headerParts = append(headerParts,
385			lipgloss.JoinHorizontal(
386				lipgloss.Left,
387				fileKey,
388				filePath,
389			),
390			baseStyle.Render(strings.Repeat(" ", p.width)),
391		)
392	case tools.LSToolName:
393		params := p.permission.Params.(tools.LSPermissionsParams)
394		pathKey := t.S().Muted.Render("Directory")
395		pathValue := t.S().Text.
396			Width(p.width - lipgloss.Width(pathKey)).
397			Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.Path)))
398		headerParts = append(headerParts,
399			lipgloss.JoinHorizontal(
400				lipgloss.Left,
401				pathKey,
402				pathValue,
403			),
404			baseStyle.Render(strings.Repeat(" ", p.width)),
405		)
406	}
407
408	return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
409}
410
411func (p *permissionDialogCmp) getOrGenerateContent() string {
412	// Return cached content if available and not dirty
413	if !p.contentDirty && p.cachedContent != "" {
414		return p.cachedContent
415	}
416
417	// Generate new content
418	var content string
419	switch p.permission.ToolName {
420	case tools.BashToolName:
421		content = p.generateBashContent()
422	case tools.DownloadToolName:
423		content = p.generateDownloadContent()
424	case tools.EditToolName:
425		content = p.generateEditContent()
426	case tools.WriteToolName:
427		content = p.generateWriteContent()
428	case tools.MultiEditToolName:
429		content = p.generateMultiEditContent()
430	case tools.FetchToolName:
431		content = p.generateFetchContent()
432	case tools.AgenticFetchToolName:
433		content = p.generateAgenticFetchContent()
434	case tools.ViewToolName:
435		content = p.generateViewContent()
436	case tools.LSToolName:
437		content = p.generateLSContent()
438	default:
439		content = p.generateDefaultContent()
440	}
441
442	// Cache the result
443	p.cachedContent = content
444	p.contentDirty = false
445
446	return content
447}
448
449func (p *permissionDialogCmp) generateBashContent() string {
450	t := styles.CurrentTheme()
451	baseStyle := t.S().Base.Background(t.BgSubtle)
452	if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
453		content := pr.Command
454		t := styles.CurrentTheme()
455		content = strings.TrimSpace(content)
456		lines := strings.Split(content, "\n")
457
458		width := p.width - 4
459		var out []string
460		for _, ln := range lines {
461			out = append(out, t.S().Muted.
462				Width(width).
463				Padding(0, 3).
464				Foreground(t.FgBase).
465				Background(t.BgSubtle).
466				Render(ln))
467		}
468
469		// Use the cache for markdown rendering
470		renderedContent := strings.Join(out, "\n")
471		finalContent := baseStyle.
472			Width(p.contentViewPort.Width()).
473			Padding(1, 0).
474			Render(renderedContent)
475
476		return finalContent
477	}
478	return ""
479}
480
481func (p *permissionDialogCmp) generateEditContent() string {
482	if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
483		formatter := core.DiffFormatter().
484			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
485			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
486			Height(p.contentViewPort.Height()).
487			Width(p.contentViewPort.Width()).
488			XOffset(p.diffXOffset).
489			YOffset(p.diffYOffset)
490		if p.useDiffSplitMode() {
491			formatter = formatter.Split()
492		} else {
493			formatter = formatter.Unified()
494		}
495
496		diff := formatter.String()
497		return diff
498	}
499	return ""
500}
501
502func (p *permissionDialogCmp) generateWriteContent() string {
503	if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
504		// Use the cache for diff rendering
505		formatter := core.DiffFormatter().
506			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
507			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
508			Height(p.contentViewPort.Height()).
509			Width(p.contentViewPort.Width()).
510			XOffset(p.diffXOffset).
511			YOffset(p.diffYOffset)
512		if p.useDiffSplitMode() {
513			formatter = formatter.Split()
514		} else {
515			formatter = formatter.Unified()
516		}
517
518		diff := formatter.String()
519		return diff
520	}
521	return ""
522}
523
524func (p *permissionDialogCmp) generateDownloadContent() string {
525	t := styles.CurrentTheme()
526	baseStyle := t.S().Base.Background(t.BgSubtle)
527	if pr, ok := p.permission.Params.(tools.DownloadPermissionsParams); ok {
528		content := fmt.Sprintf("URL: %s\nFile: %s", pr.URL, fsext.PrettyPath(pr.FilePath))
529		if pr.Timeout > 0 {
530			content += fmt.Sprintf("\nTimeout: %ds", pr.Timeout)
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) generateMultiEditContent() string {
543	if pr, ok := p.permission.Params.(tools.MultiEditPermissionsParams); ok {
544		// Use the cache for diff rendering
545		formatter := core.DiffFormatter().
546			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
547			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
548			Height(p.contentViewPort.Height()).
549			Width(p.contentViewPort.Width()).
550			XOffset(p.diffXOffset).
551			YOffset(p.diffYOffset)
552		if p.useDiffSplitMode() {
553			formatter = formatter.Split()
554		} else {
555			formatter = formatter.Unified()
556		}
557
558		diff := formatter.String()
559		return diff
560	}
561	return ""
562}
563
564func (p *permissionDialogCmp) generateFetchContent() string {
565	t := styles.CurrentTheme()
566	baseStyle := t.S().Base.Background(t.BgSubtle)
567	if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
568		finalContent := baseStyle.
569			Padding(1, 2).
570			Width(p.contentViewPort.Width()).
571			Render(pr.URL)
572		return finalContent
573	}
574	return ""
575}
576
577func (p *permissionDialogCmp) generateAgenticFetchContent() string {
578	t := styles.CurrentTheme()
579	baseStyle := t.S().Base.Background(t.BgSubtle)
580	if pr, ok := p.permission.Params.(tools.AgenticFetchPermissionsParams); ok {
581		content := fmt.Sprintf("URL: %s\n\nPrompt: %s", pr.URL, pr.Prompt)
582		finalContent := baseStyle.
583			Padding(1, 2).
584			Width(p.contentViewPort.Width()).
585			Render(content)
586		return finalContent
587	}
588	return ""
589}
590
591func (p *permissionDialogCmp) generateViewContent() string {
592	t := styles.CurrentTheme()
593	baseStyle := t.S().Base.Background(t.BgSubtle)
594	if pr, ok := p.permission.Params.(tools.ViewPermissionsParams); ok {
595		content := fmt.Sprintf("File: %s", fsext.PrettyPath(pr.FilePath))
596		if pr.Offset > 0 {
597			content += fmt.Sprintf("\nStarting from line: %d", pr.Offset+1)
598		}
599		if pr.Limit > 0 && pr.Limit != 2000 { // 2000 is the default limit
600			content += fmt.Sprintf("\nLines to read: %d", pr.Limit)
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) generateLSContent() string {
613	t := styles.CurrentTheme()
614	baseStyle := t.S().Base.Background(t.BgSubtle)
615	if pr, ok := p.permission.Params.(tools.LSPermissionsParams); ok {
616		content := fmt.Sprintf("Directory: %s", fsext.PrettyPath(pr.Path))
617		if len(pr.Ignore) > 0 {
618			content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(pr.Ignore, ", "))
619		}
620
621		finalContent := baseStyle.
622			Padding(1, 2).
623			Width(p.contentViewPort.Width()).
624			Render(content)
625		return finalContent
626	}
627	return ""
628}
629
630func (p *permissionDialogCmp) generateDefaultContent() string {
631	t := styles.CurrentTheme()
632	baseStyle := t.S().Base.Background(t.BgSubtle)
633
634	content := p.permission.Description
635
636	// Add pretty-printed JSON parameters for MCP tools
637	if p.permission.Params != nil {
638		var paramStr string
639
640		// Ensure params is a string
641		if str, ok := p.permission.Params.(string); ok {
642			paramStr = str
643		} else {
644			paramStr = fmt.Sprintf("%v", p.permission.Params)
645		}
646
647		// Try to parse as JSON for pretty printing
648		var parsed any
649		if err := json.Unmarshal([]byte(paramStr), &parsed); err == nil {
650			if b, err := json.MarshalIndent(parsed, "", "  "); err == nil {
651				if content != "" {
652					content += "\n\n"
653				}
654				content += string(b)
655			}
656		} else {
657			// Not JSON, show as-is
658			if content != "" {
659				content += "\n\n"
660			}
661			content += paramStr
662		}
663	}
664
665	content = strings.TrimSpace(content)
666	content = "\n" + content + "\n"
667	lines := strings.Split(content, "\n")
668
669	width := p.width - 4
670	var out []string
671	for _, ln := range lines {
672		ln = " " + ln // left padding
673		if len(ln) > width {
674			ln = ansi.Truncate(ln, width, "…")
675		}
676		out = append(out, t.S().Muted.
677			Width(width).
678			Foreground(t.FgBase).
679			Background(t.BgSubtle).
680			Render(ln))
681	}
682
683	// Use the cache for markdown rendering
684	renderedContent := strings.Join(out, "\n")
685	finalContent := baseStyle.
686		Width(p.contentViewPort.Width()).
687		Render(renderedContent)
688
689	if renderedContent == "" {
690		return ""
691	}
692
693	return finalContent
694}
695
696func (p *permissionDialogCmp) useDiffSplitMode() bool {
697	if p.diffSplitMode != nil {
698		return *p.diffSplitMode
699	}
700	return p.defaultDiffSplitMode
701}
702
703func (p *permissionDialogCmp) styleViewport() string {
704	t := styles.CurrentTheme()
705	return t.S().Base.Render(p.contentViewPort.View())
706}
707
708func (p *permissionDialogCmp) render() string {
709	t := styles.CurrentTheme()
710	baseStyle := t.S().Base
711	title := core.Title("Permission Required", p.width-4)
712	// Render header
713	headerContent := p.renderHeader()
714	// Render buttons
715	buttons := p.renderButtons()
716
717	p.contentViewPort.SetWidth(p.width - 4)
718
719	// Get cached or generate content
720	contentFinal := p.getOrGenerateContent()
721
722	// Always set viewport content (the caching is handled in getOrGenerateContent)
723	const minContentHeight = 9
724	contentHeight := min(
725		max(minContentHeight, p.height-minContentHeight),
726		lipgloss.Height(contentFinal),
727	)
728	p.contentViewPort.SetHeight(contentHeight)
729	p.contentViewPort.SetContent(contentFinal)
730
731	p.positionRow = p.wHeight / 2
732	p.positionRow -= (contentHeight + 9) / 2
733	p.positionRow -= 3 // Move dialog slightly higher than middle
734
735	var contentHelp string
736	if p.supportsDiffView() {
737		contentHelp = help.New().View(p.keyMap)
738	}
739
740	// Calculate content height dynamically based on window size
741	strs := []string{
742		title,
743		"",
744		headerContent,
745		p.styleViewport(),
746		"",
747		buttons,
748		"",
749	}
750	if contentHelp != "" {
751		strs = append(strs, "", contentHelp)
752	}
753	content := lipgloss.JoinVertical(lipgloss.Top, strs...)
754
755	dialog := baseStyle.
756		Padding(0, 1).
757		Border(lipgloss.RoundedBorder()).
758		BorderForeground(t.BorderFocus).
759		Width(p.width).
760		Render(
761			content,
762		)
763	p.finalDialogHeight = lipgloss.Height(dialog)
764	return dialog
765}
766
767func (p *permissionDialogCmp) View() string {
768	return p.render()
769}
770
771func (p *permissionDialogCmp) SetSize() tea.Cmd {
772	if p.permission.ID == "" {
773		return nil
774	}
775
776	oldWidth, oldHeight := p.width, p.height
777
778	switch p.permission.ToolName {
779	case tools.BashToolName:
780		p.width = int(float64(p.wWidth) * 0.8)
781		p.height = int(float64(p.wHeight) * 0.3)
782	case tools.DownloadToolName:
783		p.width = int(float64(p.wWidth) * 0.8)
784		p.height = int(float64(p.wHeight) * 0.4)
785	case tools.EditToolName:
786		p.width = int(float64(p.wWidth) * 0.8)
787		p.height = int(float64(p.wHeight) * 0.8)
788	case tools.WriteToolName:
789		p.width = int(float64(p.wWidth) * 0.8)
790		p.height = int(float64(p.wHeight) * 0.8)
791	case tools.MultiEditToolName:
792		p.width = int(float64(p.wWidth) * 0.8)
793		p.height = int(float64(p.wHeight) * 0.8)
794	case tools.FetchToolName:
795		p.width = int(float64(p.wWidth) * 0.8)
796		p.height = int(float64(p.wHeight) * 0.3)
797	case tools.AgenticFetchToolName:
798		p.width = int(float64(p.wWidth) * 0.8)
799		p.height = int(float64(p.wHeight) * 0.4)
800	case tools.ViewToolName:
801		p.width = int(float64(p.wWidth) * 0.8)
802		p.height = int(float64(p.wHeight) * 0.4)
803	case tools.LSToolName:
804		p.width = int(float64(p.wWidth) * 0.8)
805		p.height = int(float64(p.wHeight) * 0.4)
806	default:
807		p.width = int(float64(p.wWidth) * 0.7)
808		p.height = int(float64(p.wHeight) * 0.5)
809	}
810
811	// Default to diff split mode when dialog is wide enough.
812	p.defaultDiffSplitMode = p.width >= 140
813
814	// Set a maximum width for the dialog
815	p.width = min(p.width, 180)
816
817	// Mark content as dirty if size changed
818	if oldWidth != p.width || oldHeight != p.height {
819		p.contentDirty = true
820	}
821	p.positionRow = p.wHeight / 2
822	p.positionRow -= p.height / 2
823	p.positionRow -= 3 // Move dialog slightly higher than middle
824	p.positionCol = p.wWidth / 2
825	p.positionCol -= p.width / 2
826	return nil
827}
828
829func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
830	content, err := generator()
831	if err != nil {
832		return fmt.Sprintf("Error rendering markdown: %v", err)
833	}
834
835	return content
836}
837
838// ID implements PermissionDialogCmp.
839func (p *permissionDialogCmp) ID() dialogs.DialogID {
840	return PermissionsDialogID
841}
842
843// Position implements PermissionDialogCmp.
844func (p *permissionDialogCmp) Position() (int, int) {
845	return p.positionRow, p.positionCol
846}
847
848// Options for create a new permission dialog
849type Options struct {
850	DiffMode string // split or unified, empty means use defaultDiffSplitMode
851}
852
853// isSplitMode returns internal representation of diff mode switch
854func (o Options) isSplitMode() *bool {
855	var split bool
856
857	switch o.DiffMode {
858	case "split":
859		split = true
860	case "unified":
861		split = false
862	default:
863		return nil
864	}
865
866	return &split
867}