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) (tea.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.ViewToolName:
377 params := p.permission.Params.(tools.ViewPermissionsParams)
378 fileKey := t.S().Muted.Render("File")
379 filePath := t.S().Text.
380 Width(p.width - lipgloss.Width(fileKey)).
381 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
382 headerParts = append(headerParts,
383 lipgloss.JoinHorizontal(
384 lipgloss.Left,
385 fileKey,
386 filePath,
387 ),
388 baseStyle.Render(strings.Repeat(" ", p.width)),
389 )
390 case tools.LSToolName:
391 params := p.permission.Params.(tools.LSPermissionsParams)
392 pathKey := t.S().Muted.Render("Directory")
393 pathValue := t.S().Text.
394 Width(p.width - lipgloss.Width(pathKey)).
395 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.Path)))
396 headerParts = append(headerParts,
397 lipgloss.JoinHorizontal(
398 lipgloss.Left,
399 pathKey,
400 pathValue,
401 ),
402 baseStyle.Render(strings.Repeat(" ", p.width)),
403 )
404 }
405
406 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
407}
408
409func (p *permissionDialogCmp) getOrGenerateContent() string {
410 // Return cached content if available and not dirty
411 if !p.contentDirty && p.cachedContent != "" {
412 return p.cachedContent
413 }
414
415 // Generate new content
416 var content string
417 switch p.permission.ToolName {
418 case tools.BashToolName:
419 content = p.generateBashContent()
420 case tools.DownloadToolName:
421 content = p.generateDownloadContent()
422 case tools.EditToolName:
423 content = p.generateEditContent()
424 case tools.WriteToolName:
425 content = p.generateWriteContent()
426 case tools.MultiEditToolName:
427 content = p.generateMultiEditContent()
428 case tools.FetchToolName:
429 content = p.generateFetchContent()
430 case tools.ViewToolName:
431 content = p.generateViewContent()
432 case tools.LSToolName:
433 content = p.generateLSContent()
434 default:
435 content = p.generateDefaultContent()
436 }
437
438 // Cache the result
439 p.cachedContent = content
440 p.contentDirty = false
441
442 return content
443}
444
445func (p *permissionDialogCmp) generateBashContent() string {
446 t := styles.CurrentTheme()
447 baseStyle := t.S().Base.Background(t.BgSubtle)
448 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
449 content := pr.Command
450 t := styles.CurrentTheme()
451 content = strings.TrimSpace(content)
452 lines := strings.Split(content, "\n")
453
454 width := p.width - 4
455 var out []string
456 for _, ln := range lines {
457 out = append(out, t.S().Muted.
458 Width(width).
459 Padding(0, 3).
460 Foreground(t.FgBase).
461 Background(t.BgSubtle).
462 Render(ln))
463 }
464
465 // Use the cache for markdown rendering
466 renderedContent := strings.Join(out, "\n")
467 finalContent := baseStyle.
468 Width(p.contentViewPort.Width()).
469 Padding(1, 0).
470 Render(renderedContent)
471
472 return finalContent
473 }
474 return ""
475}
476
477func (p *permissionDialogCmp) generateEditContent() string {
478 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
479 formatter := core.DiffFormatter().
480 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
481 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
482 Height(p.contentViewPort.Height()).
483 Width(p.contentViewPort.Width()).
484 XOffset(p.diffXOffset).
485 YOffset(p.diffYOffset)
486 if p.useDiffSplitMode() {
487 formatter = formatter.Split()
488 } else {
489 formatter = formatter.Unified()
490 }
491
492 diff := formatter.String()
493 return diff
494 }
495 return ""
496}
497
498func (p *permissionDialogCmp) generateWriteContent() string {
499 if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
500 // Use the cache for diff rendering
501 formatter := core.DiffFormatter().
502 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
503 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
504 Height(p.contentViewPort.Height()).
505 Width(p.contentViewPort.Width()).
506 XOffset(p.diffXOffset).
507 YOffset(p.diffYOffset)
508 if p.useDiffSplitMode() {
509 formatter = formatter.Split()
510 } else {
511 formatter = formatter.Unified()
512 }
513
514 diff := formatter.String()
515 return diff
516 }
517 return ""
518}
519
520func (p *permissionDialogCmp) generateDownloadContent() string {
521 t := styles.CurrentTheme()
522 baseStyle := t.S().Base.Background(t.BgSubtle)
523 if pr, ok := p.permission.Params.(tools.DownloadPermissionsParams); ok {
524 content := fmt.Sprintf("URL: %s\nFile: %s", pr.URL, fsext.PrettyPath(pr.FilePath))
525 if pr.Timeout > 0 {
526 content += fmt.Sprintf("\nTimeout: %ds", pr.Timeout)
527 }
528
529 finalContent := baseStyle.
530 Padding(1, 2).
531 Width(p.contentViewPort.Width()).
532 Render(content)
533 return finalContent
534 }
535 return ""
536}
537
538func (p *permissionDialogCmp) generateMultiEditContent() string {
539 if pr, ok := p.permission.Params.(tools.MultiEditPermissionsParams); ok {
540 // Use the cache for diff rendering
541 formatter := core.DiffFormatter().
542 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
543 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
544 Height(p.contentViewPort.Height()).
545 Width(p.contentViewPort.Width()).
546 XOffset(p.diffXOffset).
547 YOffset(p.diffYOffset)
548 if p.useDiffSplitMode() {
549 formatter = formatter.Split()
550 } else {
551 formatter = formatter.Unified()
552 }
553
554 diff := formatter.String()
555 return diff
556 }
557 return ""
558}
559
560func (p *permissionDialogCmp) generateFetchContent() string {
561 t := styles.CurrentTheme()
562 baseStyle := t.S().Base.Background(t.BgSubtle)
563 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
564 finalContent := baseStyle.
565 Padding(1, 2).
566 Width(p.contentViewPort.Width()).
567 Render(pr.URL)
568 return finalContent
569 }
570 return ""
571}
572
573func (p *permissionDialogCmp) generateViewContent() string {
574 t := styles.CurrentTheme()
575 baseStyle := t.S().Base.Background(t.BgSubtle)
576 if pr, ok := p.permission.Params.(tools.ViewPermissionsParams); ok {
577 content := fmt.Sprintf("File: %s", fsext.PrettyPath(pr.FilePath))
578 if pr.Offset > 0 {
579 content += fmt.Sprintf("\nStarting from line: %d", pr.Offset+1)
580 }
581 if pr.Limit > 0 && pr.Limit != 2000 { // 2000 is the default limit
582 content += fmt.Sprintf("\nLines to read: %d", pr.Limit)
583 }
584
585 finalContent := baseStyle.
586 Padding(1, 2).
587 Width(p.contentViewPort.Width()).
588 Render(content)
589 return finalContent
590 }
591 return ""
592}
593
594func (p *permissionDialogCmp) generateLSContent() string {
595 t := styles.CurrentTheme()
596 baseStyle := t.S().Base.Background(t.BgSubtle)
597 if pr, ok := p.permission.Params.(tools.LSPermissionsParams); ok {
598 content := fmt.Sprintf("Directory: %s", fsext.PrettyPath(pr.Path))
599 if len(pr.Ignore) > 0 {
600 content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(pr.Ignore, ", "))
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) generateDefaultContent() string {
613 t := styles.CurrentTheme()
614 baseStyle := t.S().Base.Background(t.BgSubtle)
615
616 content := p.permission.Description
617
618 // Add pretty-printed JSON parameters for MCP tools
619 if p.permission.Params != nil {
620 var paramStr string
621
622 // Ensure params is a string
623 if str, ok := p.permission.Params.(string); ok {
624 paramStr = str
625 } else {
626 paramStr = fmt.Sprintf("%v", p.permission.Params)
627 }
628
629 // Try to parse as JSON for pretty printing
630 var parsed any
631 if err := json.Unmarshal([]byte(paramStr), &parsed); err == nil {
632 if b, err := json.MarshalIndent(parsed, "", " "); err == nil {
633 if content != "" {
634 content += "\n\n"
635 }
636 content += string(b)
637 }
638 } else {
639 // Not JSON, show as-is
640 if content != "" {
641 content += "\n\n"
642 }
643 content += paramStr
644 }
645 }
646
647 content = strings.TrimSpace(content)
648 content = "\n" + content + "\n"
649 lines := strings.Split(content, "\n")
650
651 width := p.width - 4
652 var out []string
653 for _, ln := range lines {
654 ln = " " + ln // left padding
655 if len(ln) > width {
656 ln = ansi.Truncate(ln, width, "…")
657 }
658 out = append(out, t.S().Muted.
659 Width(width).
660 Foreground(t.FgBase).
661 Background(t.BgSubtle).
662 Render(ln))
663 }
664
665 // Use the cache for markdown rendering
666 renderedContent := strings.Join(out, "\n")
667 finalContent := baseStyle.
668 Width(p.contentViewPort.Width()).
669 Render(renderedContent)
670
671 if renderedContent == "" {
672 return ""
673 }
674
675 return finalContent
676}
677
678func (p *permissionDialogCmp) useDiffSplitMode() bool {
679 if p.diffSplitMode != nil {
680 return *p.diffSplitMode
681 }
682 return p.defaultDiffSplitMode
683}
684
685func (p *permissionDialogCmp) styleViewport() string {
686 t := styles.CurrentTheme()
687 return t.S().Base.Render(p.contentViewPort.View())
688}
689
690func (p *permissionDialogCmp) render() string {
691 t := styles.CurrentTheme()
692 baseStyle := t.S().Base
693 title := core.Title("Permission Required", p.width-4)
694 // Render header
695 headerContent := p.renderHeader()
696 // Render buttons
697 buttons := p.renderButtons()
698
699 p.contentViewPort.SetWidth(p.width - 4)
700
701 // Get cached or generate content
702 contentFinal := p.getOrGenerateContent()
703
704 // Always set viewport content (the caching is handled in getOrGenerateContent)
705 const minContentHeight = 9
706 contentHeight := min(
707 max(minContentHeight, p.height-minContentHeight),
708 lipgloss.Height(contentFinal),
709 )
710 p.contentViewPort.SetHeight(contentHeight)
711 p.contentViewPort.SetContent(contentFinal)
712
713 p.positionRow = p.wHeight / 2
714 p.positionRow -= (contentHeight + 9) / 2
715 p.positionRow -= 3 // Move dialog slightly higher than middle
716
717 var contentHelp string
718 if p.supportsDiffView() {
719 contentHelp = help.New().View(p.keyMap)
720 }
721
722 // Calculate content height dynamically based on window size
723 strs := []string{
724 title,
725 "",
726 headerContent,
727 p.styleViewport(),
728 "",
729 buttons,
730 "",
731 }
732 if contentHelp != "" {
733 strs = append(strs, "", contentHelp)
734 }
735 content := lipgloss.JoinVertical(lipgloss.Top, strs...)
736
737 dialog := baseStyle.
738 Padding(0, 1).
739 Border(lipgloss.RoundedBorder()).
740 BorderForeground(t.BorderFocus).
741 Width(p.width).
742 Render(
743 content,
744 )
745 p.finalDialogHeight = lipgloss.Height(dialog)
746 return dialog
747}
748
749func (p *permissionDialogCmp) View() string {
750 return p.render()
751}
752
753func (p *permissionDialogCmp) SetSize() tea.Cmd {
754 if p.permission.ID == "" {
755 return nil
756 }
757
758 oldWidth, oldHeight := p.width, p.height
759
760 switch p.permission.ToolName {
761 case tools.BashToolName:
762 p.width = int(float64(p.wWidth) * 0.8)
763 p.height = int(float64(p.wHeight) * 0.3)
764 case tools.DownloadToolName:
765 p.width = int(float64(p.wWidth) * 0.8)
766 p.height = int(float64(p.wHeight) * 0.4)
767 case tools.EditToolName:
768 p.width = int(float64(p.wWidth) * 0.8)
769 p.height = int(float64(p.wHeight) * 0.8)
770 case tools.WriteToolName:
771 p.width = int(float64(p.wWidth) * 0.8)
772 p.height = int(float64(p.wHeight) * 0.8)
773 case tools.MultiEditToolName:
774 p.width = int(float64(p.wWidth) * 0.8)
775 p.height = int(float64(p.wHeight) * 0.8)
776 case tools.FetchToolName:
777 p.width = int(float64(p.wWidth) * 0.8)
778 p.height = int(float64(p.wHeight) * 0.3)
779 case tools.ViewToolName:
780 p.width = int(float64(p.wWidth) * 0.8)
781 p.height = int(float64(p.wHeight) * 0.4)
782 case tools.LSToolName:
783 p.width = int(float64(p.wWidth) * 0.8)
784 p.height = int(float64(p.wHeight) * 0.4)
785 default:
786 p.width = int(float64(p.wWidth) * 0.7)
787 p.height = int(float64(p.wHeight) * 0.5)
788 }
789
790 // Default to diff split mode when dialog is wide enough.
791 p.defaultDiffSplitMode = p.width >= 140
792
793 // Set a maximum width for the dialog
794 p.width = min(p.width, 180)
795
796 // Mark content as dirty if size changed
797 if oldWidth != p.width || oldHeight != p.height {
798 p.contentDirty = true
799 }
800 p.positionRow = p.wHeight / 2
801 p.positionRow -= p.height / 2
802 p.positionRow -= 3 // Move dialog slightly higher than middle
803 p.positionCol = p.wWidth / 2
804 p.positionCol -= p.width / 2
805 return nil
806}
807
808func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
809 content, err := generator()
810 if err != nil {
811 return fmt.Sprintf("Error rendering markdown: %v", err)
812 }
813
814 return content
815}
816
817// ID implements PermissionDialogCmp.
818func (p *permissionDialogCmp) ID() dialogs.DialogID {
819 return PermissionsDialogID
820}
821
822// Position implements PermissionDialogCmp.
823func (p *permissionDialogCmp) Position() (int, int) {
824 return p.positionRow, p.positionCol
825}
826
827// Options for create a new permission dialog
828type Options struct {
829 DiffMode string // split or unified, empty means use defaultDiffSplitMode
830}
831
832// isSplitMode returns internal representation of diff mode switch
833func (o Options) isSplitMode() *bool {
834 var split bool
835
836 switch o.DiffMode {
837 case "split":
838 split = true
839 case "unified":
840 split = false
841 default:
842 return nil
843 }
844
845 return &split
846}