permissions.go

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