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