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}