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}