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