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