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