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