@@ -13,7 +13,9 @@ import (
1313 "github.com/charmbracelet/crush/internal/agent/tools"
1414 "github.com/charmbracelet/crush/internal/fsext"
1515 "github.com/charmbracelet/crush/internal/permission"
16+ "github.com/charmbracelet/crush/internal/stringext"
1617 "github.com/charmbracelet/crush/internal/ui/common"
18+ "github.com/charmbracelet/crush/internal/ui/styles"
1719 uv "github.com/charmbracelet/ultraviolet"
1820)
1921
@@ -314,19 +316,19 @@ func (p *Permissions) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
314316 forceFullscreen := area.Dx() <= minWindowWidth || area.Dy() <= minWindowHeight
315317
316318 // Calculate dialog dimensions based on fullscreen state and content type.
317− var width, height int
319+ var width, maxHeight int
318320 if forceFullscreen || (p.fullscreen && p.hasDiffView()) {
319321 // Use nearly full window for fullscreen.
320322 width = area.Dx()
321− height = area.Dy()
323+ maxHeight = area.Dy()
322324 } else if p.hasDiffView() {
323325 // Wide for side-by-side diffs, capped for readability.
324326 width = min(int(float64(area.Dx())*diffSizeRatio), diffMaxWidth)
325− height = int(float64(area.Dy()) * diffSizeRatio)
327+ maxHeight = int(float64(area.Dy()) * diffSizeRatio)
326328 } else {
327329 // Narrower for simple content like commands/URLs.
328330 width = min(int(float64(area.Dx())*simpleSizeRatio), simpleMaxWidth)
329− height = int(float64(area.Dy()) * simpleHeightRatio)
331+ maxHeight = int(float64(area.Dy()) * simpleHeightRatio)
330332 }
331333
332334 dialogStyle := t.Dialog.View.Width(width).Padding(0, 1)
@@ -341,27 +343,51 @@ func (p *Permissions) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
341343 buttonsHeight := lipgloss.Height(buttons)
342344 helpHeight := lipgloss.Height(helpView)
343345 frameHeight := dialogStyle.GetVerticalFrameSize() + layoutSpacingLines
344− availableHeight := height - headerHeight - buttonsHeight - helpHeight - frameHeight
345346
346347 p.defaultDiffSplitMode = width >= splitModeMinWidth
347348
348− if p.viewport.Width() != contentWidth-1 {
349− // Mark diff content as dirty if width has changed
349+ // Pre-render content to measure its actual height.
350+ renderedContent := p.renderContent(contentWidth)
351+ contentHeight := lipgloss.Height(renderedContent)
352+
353+ // For non-diff views, shrink dialog to fit content if it's smaller than max.
354+ var availableHeight int
355+ if !p.hasDiffView() && !forceFullscreen {
356+ fixedHeight := headerHeight + buttonsHeight + helpHeight + frameHeight
357+ neededHeight := fixedHeight + contentHeight
358+ if neededHeight < maxHeight {
359+ availableHeight = contentHeight
360+ } else {
361+ availableHeight = maxHeight - fixedHeight
362+ }
363+ } else {
364+ availableHeight = maxHeight - headerHeight - buttonsHeight - helpHeight - frameHeight
365+ }
366+
367+ // Determine if scrollbar is needed.
368+ needsScrollbar := p.hasDiffView() || contentHeight > availableHeight
369+ viewportWidth := contentWidth
370+ if needsScrollbar {
371+ viewportWidth = contentWidth - 1 // Reserve space for scrollbar.
372+ }
373+
374+ if p.viewport.Width() != viewportWidth {
375+ // Mark content as dirty if width has changed.
350376 p.viewportDirty = true
377+ renderedContent = p.renderContent(viewportWidth)
351378 }
352379
353380 var content string
354381 var scrollbar string
355− // Non-diff content uses the viewport for scrolling.
356− p.viewport.SetWidth(contentWidth - 1) // -1 for scrollbar
382+ p.viewport.SetWidth(viewportWidth)
357383 p.viewport.SetHeight(availableHeight)
358384 if p.viewportDirty {
359− p.viewport.SetContent(p.renderContent(contentWidth - 1))
385+ p.viewport.SetContent(renderedContent)
360386 p.viewportWidth = p.viewport.Width()
361387 p.viewportDirty = false
362388 }
363389 content = p.viewport.View()
364− if p.canScroll() {
390+ if needsScrollbar {
365391 scrollbar = common.Scrollbar(t, availableHeight, p.viewport.TotalLineCount(), availableHeight, p.viewport.YOffset())
366392 }
367393
@@ -388,7 +414,7 @@ func (p *Permissions) renderHeader(contentWidth int) string {
388414 title = t.Dialog.Title.Render(title)
389415
390416 // Tool info.
391− toolLine := p.renderKeyValue("Tool", p.permission.ToolName, contentWidth)
417+ toolLine := p.renderToolName(contentWidth)
392418 pathLine := p.renderKeyValue("Path", fsext.PrettyPath(p.permission.Path), contentWidth)
393419
394420 lines := []string{title, "", toolLine, pathLine}
@@ -439,10 +465,33 @@ func (p *Permissions) renderKeyValue(key, value string, width int) string {
439465 return lipgloss.JoinHorizontal(lipgloss.Left, keyStr, valueStr)
440466}
441467
468+func (p *Permissions) renderToolName(width int) string {
469+ toolName := p.permission.ToolName
470+
471+ // Check if this is an MCP tool (format: mcp_<mcpname>_<toolname>).
472+ if strings.HasPrefix(toolName, "mcp_") {
473+ parts := strings.SplitN(toolName, "_", 3)
474+ if len(parts) == 3 {
475+ mcpName := prettyName(parts[1])
476+ toolPart := prettyName(parts[2])
477+ toolName = fmt.Sprintf("%s %s %s", mcpName, styles.ArrowRightIcon, toolPart)
478+ }
479+ }
480+
481+ return p.renderKeyValue("Tool", toolName, width)
482+}
483+
484+// prettyName converts snake_case or kebab-case to Title Case.
485+func prettyName(name string) string {
486+ name = strings.ReplaceAll(name, "_", " ")
487+ name = strings.ReplaceAll(name, "-", " ")
488+ return stringext.Capitalize(name)
489+}
490+
442491func (p *Permissions) renderContent(width int) string {
443492 switch p.permission.ToolName {
444493 case tools.BashToolName:
445− return p.renderBashContent()
494+ return p.renderBashContent(width)
446495 case tools.EditToolName:
447496 return p.renderEditContent(width)
448497 case tools.WriteToolName:
@@ -450,27 +499,27 @@ func (p *Permissions) renderContent(width int) string {
450499 case tools.MultiEditToolName:
451500 return p.renderMultiEditContent(width)
452501 case tools.DownloadToolName:
453− return p.renderDownloadContent()
502+ return p.renderDownloadContent(width)
454503 case tools.FetchToolName:
455− return p.renderFetchContent()
504+ return p.renderFetchContent(width)
456505 case tools.AgenticFetchToolName:
457− return p.renderAgenticFetchContent()
506+ return p.renderAgenticFetchContent(width)
458507 case tools.ViewToolName:
459− return p.renderViewContent()
508+ return p.renderViewContent(width)
460509 case tools.LSToolName:
461− return p.renderLSContent()
510+ return p.renderLSContent(width)
462511 default:
463− return p.renderDefaultContent()
512+ return p.renderDefaultContent(width)
464513 }
465514}
466515
467−func (p *Permissions) renderBashContent() string {
516+func (p *Permissions) renderBashContent(width int) string {
468517 params, ok := p.permission.Params.(tools.BashPermissionsParams)
469518 if !ok {
470519 return ""
471520 }
472521
473− return p.com.Styles.Dialog.ContentPanel.Render(params.Command)
522+ return p.renderContentPanel(params.Command, width)
474523}
475524
476525func (p *Permissions) renderEditContent(contentWidth int) string {
@@ -529,7 +578,7 @@ func (p *Permissions) renderDiff(filePath, oldContent, newContent string, conten
529578 return result
530579}
531580
532−func (p *Permissions) renderDownloadContent() string {
581+func (p *Permissions) renderDownloadContent(width int) string {
533582 params, ok := p.permission.Params.(tools.DownloadPermissionsParams)
534583 if !ok {
535584 return ""
@@ -540,19 +589,19 @@ func (p *Permissions) renderDownloadContent() string {
540589 content += fmt.Sprintf("\nTimeout: %ds", params.Timeout)
541590 }
542591
543− return p.com.Styles.Dialog.ContentPanel.Render(content)
592+ return p.renderContentPanel(content, width)
544593}
545594
546−func (p *Permissions) renderFetchContent() string {
595+func (p *Permissions) renderFetchContent(width int) string {
547596 params, ok := p.permission.Params.(tools.FetchPermissionsParams)
548597 if !ok {
549598 return ""
550599 }
551600
552− return p.com.Styles.Dialog.ContentPanel.Render(params.URL)
601+ return p.renderContentPanel(params.URL, width)
553602}
554603
555−func (p *Permissions) renderAgenticFetchContent() string {
604+func (p *Permissions) renderAgenticFetchContent(width int) string {
556605 params, ok := p.permission.Params.(tools.AgenticFetchPermissionsParams)
557606 if !ok {
558607 return ""
@@ -565,10 +614,10 @@ func (p *Permissions) renderAgenticFetchContent() string {
565614 content = fmt.Sprintf("Prompt: %s", params.Prompt)
566615 }
567616
568− return p.com.Styles.Dialog.ContentPanel.Render(content)
617+ return p.renderContentPanel(content, width)
569618}
570619
571−func (p *Permissions) renderViewContent() string {
620+func (p *Permissions) renderViewContent(width int) string {
572621 params, ok := p.permission.Params.(tools.ViewPermissionsParams)
573622 if !ok {
574623 return ""
@@ -582,10 +631,10 @@ func (p *Permissions) renderViewContent() string {
582631 content += fmt.Sprintf("\nLines to read: %d", params.Limit)
583632 }
584633
585− return p.com.Styles.Dialog.ContentPanel.Render(content)
634+ return p.renderContentPanel(content, width)
586635}
587636
588−func (p *Permissions) renderLSContent() string {
637+func (p *Permissions) renderLSContent(width int) string {
589638 params, ok := p.permission.Params.(tools.LSPermissionsParams)
590639 if !ok {
591640 return ""
@@ -596,11 +645,16 @@ func (p *Permissions) renderLSContent() string {
596645 content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(params.Ignore, ", "))
597646 }
598647
599− return p.com.Styles.Dialog.ContentPanel.Render(content)
648+ return p.renderContentPanel(content, width)
600649}
601650
602−func (p *Permissions) renderDefaultContent() string {
603− content := p.permission.Description
651+func (p *Permissions) renderDefaultContent(width int) string {
652+ t := p.com.Styles
653+ var content string
654+ // do not add the description for mcp tools
655+ if !strings.HasPrefix(p.permission.ToolName, "mcp_") {
656+ content = p.permission.Description
657+ }
604658
605659 // Pretty-print JSON params if available.
606660 if p.permission.Params != nil {
@@ -614,10 +668,15 @@ func (p *Permissions) renderDefaultContent() string {
614668 var parsed any
615669 if err := json.Unmarshal([]byte(paramStr), &parsed); err == nil {
616670 if b, err := json.MarshalIndent(parsed, "", " "); err == nil {
671+ jsonContent := string(b)
672+ highlighted, err := common.SyntaxHighlight(t, jsonContent, "params.json", t.BgSubtle)
673+ if err == nil {
674+ jsonContent = highlighted
675+ }
617676 if content != "" {
618677 content += "\n\n"
619678 }
620− content += string(b)
679+ content += jsonContent
621680 }
622681 } else if paramStr != "" {
623682 if content != "" {
@@ -631,7 +690,13 @@ func (p *Permissions) renderDefaultContent() string {
631690 return ""
632691 }
633692
634− return p.com.Styles.Dialog.ContentPanel.Render(strings.TrimSpace(content))
693+ return p.renderContentPanel(strings.TrimSpace(content), width)
694+}
695+
696+// renderContentPanel renders content in a panel with the full width.
697+func (p *Permissions) renderContentPanel(content string, width int) string {
698+ panelStyle := p.com.Styles.Dialog.ContentPanel
699+ return panelStyle.Width(width).Render(content)
635700}
636701
637702func (p *Permissions) renderButtons(contentWidth int) string {