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