11package dialog
22
33import (
4- "charm.land/bubbles/v2/key"
5- tea "charm.land/bubbletea/v2"
64 "charm.land/lipgloss/v2"
75
86 "github.com/docker/docker-agent/pkg/runtime"
9- "github.com/docker/docker-agent/pkg/tui/components/scrollview"
10- "github.com/docker/docker-agent/pkg/tui/core"
11- "github.com/docker/docker-agent/pkg/tui/core/layout"
127 "github.com/docker/docker-agent/pkg/tui/styles"
138)
149
1510// permissionsDialog displays the configured tool permissions (allow/deny patterns).
1611type permissionsDialog struct {
17- BaseDialog
12+ readOnlyScrollDialog
1813
1914 permissions * runtime.PermissionsInfo
2015 yoloEnabled bool
21- closeKey key.Binding
22- scrollview * scrollview.Model
2316}
2417
2518// NewPermissionsDialog creates a new dialog showing tool permission rules.
2619func NewPermissionsDialog (perms * runtime.PermissionsInfo , yoloEnabled bool ) Dialog {
27- return & permissionsDialog {
20+ d := & permissionsDialog {
2821 permissions : perms ,
2922 yoloEnabled : yoloEnabled ,
30- scrollview : scrollview .New (
31- scrollview .WithKeyMap (scrollview .ReadOnlyScrollKeyMap ()),
32- scrollview .WithReserveScrollbarSpace (true ),
33- ),
34- closeKey : key .NewBinding (key .WithKeys ("esc" , "enter" , "q" ), key .WithHelp ("Esc" , "close" )),
3523 }
24+ d .readOnlyScrollDialog = newReadOnlyScrollDialog (
25+ readOnlyScrollDialogSize {widthPercent : 60 , minWidth : 40 , maxWidth : 70 , heightPercent : 70 , heightMax : 30 },
26+ d .renderLines ,
27+ )
28+ return d
3629}
3730
38- func (d * permissionsDialog ) Init () tea.Cmd {
39- return nil
40- }
41-
42- func (d * permissionsDialog ) Update (msg tea.Msg ) (layout.Model , tea.Cmd ) {
43- if handled , cmd := d .scrollview .Update (msg ); handled {
44- return d , cmd
45- }
46-
47- switch msg := msg .(type ) {
48- case tea.WindowSizeMsg :
49- cmd := d .SetSize (msg .Width , msg .Height )
50- return d , cmd
51-
52- case tea.KeyPressMsg :
53- if key .Matches (msg , d .closeKey ) {
54- return d , core .CmdHandler (CloseDialogMsg {})
55- }
56- }
57- return d , nil
58- }
59-
60- func (d * permissionsDialog ) dialogSize () (dialogWidth , maxHeight , contentWidth int ) {
61- dialogWidth = d .ComputeDialogWidth (60 , 40 , 70 )
62- maxHeight = min (d .Height ()* 70 / 100 , 30 )
63- contentWidth = d .ContentWidth (dialogWidth , 2 ) - d .scrollview .ReservedCols ()
64- return dialogWidth , maxHeight , contentWidth
65- }
66-
67- func (d * permissionsDialog ) Position () (row , col int ) {
68- dialogWidth , maxHeight , _ := d .dialogSize ()
69- return CenterPosition (d .Width (), d .Height (), dialogWidth , maxHeight )
70- }
71-
72- func (d * permissionsDialog ) View () string {
73- dialogWidth , maxHeight , contentWidth := d .dialogSize ()
74- content := d .renderContent (contentWidth , maxHeight )
75- return styles .DialogStyle .Padding (1 , 2 ).Width (dialogWidth ).Render (content )
76- }
77-
78- func (d * permissionsDialog ) renderContent (contentWidth , maxHeight int ) string {
79- // Build all lines
31+ func (d * permissionsDialog ) renderLines (contentWidth , _ int ) []string {
8032 lines := []string {
8133 RenderTitle ("Tool Permissions" , contentWidth , styles .DialogTitleStyle ),
8234 RenderSeparator (contentWidth ),
@@ -89,7 +41,6 @@ func (d *permissionsDialog) renderContent(contentWidth, maxHeight int) string {
8941 if d .permissions == nil {
9042 lines = append (lines , styles .MutedStyle .Render ("No permission patterns configured." ), "" )
9143 } else {
92- // Deny section (checked first during evaluation)
9344 if len (d .permissions .Deny ) > 0 {
9445 lines = append (lines , d .renderSectionHeader ("Deny" , "Always blocked, even with yolo mode" ), "" )
9546 for _ , pattern := range d .permissions .Deny {
@@ -98,7 +49,6 @@ func (d *permissionsDialog) renderContent(contentWidth, maxHeight int) string {
9849 lines = append (lines , "" )
9950 }
10051
101- // Allow section
10252 if len (d .permissions .Allow ) > 0 {
10353 lines = append (lines , d .renderSectionHeader ("Allow" , "Auto-approved without confirmation" ), "" )
10454 for _ , pattern := range d .permissions .Allow {
@@ -107,7 +57,6 @@ func (d *permissionsDialog) renderContent(contentWidth, maxHeight int) string {
10757 lines = append (lines , "" )
10858 }
10959
110- // Ask section
11160 if len (d .permissions .Ask ) > 0 {
11261 lines = append (lines , d .renderSectionHeader ("Ask" , "Always requires confirmation, even for read-only tools" ), "" )
11362 for _ , pattern := range d .permissions .Ask {
@@ -116,14 +65,12 @@ func (d *permissionsDialog) renderContent(contentWidth, maxHeight int) string {
11665 lines = append (lines , "" )
11766 }
11867
119- // If all are empty
12068 if len (d .permissions .Allow ) == 0 && len (d .permissions .Ask ) == 0 && len (d .permissions .Deny ) == 0 {
12169 lines = append (lines , styles .MutedStyle .Render ("No permission patterns configured." ), "" )
12270 }
12371 }
12472
125- // Apply scrolling
126- return d .applyScrolling (lines , contentWidth , maxHeight )
73+ return lines
12774}
12875
12976func (d * permissionsDialog ) renderYoloStatus () string {
@@ -146,7 +93,6 @@ func (d *permissionsDialog) renderSectionHeader(title, description string) strin
14693}
14794
14895func (d * permissionsDialog ) renderPattern (pattern string , isDeny bool ) string {
149- // Use different colors for deny (red-ish) vs allow (green-ish)
15096 var icon string
15197 var style lipgloss.Style
15298 if isDeny {
@@ -165,26 +111,3 @@ func (d *permissionsDialog) renderAskPattern(pattern string) string {
165111 style := lipgloss .NewStyle ().Foreground (styles .TextSecondary )
166112 return style .Render (icon ) + " " + lipgloss .NewStyle ().Foreground (styles .Highlight ).Render (pattern )
167113}
168-
169- func (d * permissionsDialog ) applyScrolling (allLines []string , contentWidth , maxHeight int ) string {
170- const headerLines = 3 // title + separator + space
171- const footerLines = 2 // space + help
172-
173- visibleLines := max (1 , maxHeight - headerLines - footerLines - 4 )
174- contentLines := allLines [headerLines :]
175-
176- regionWidth := contentWidth + d .scrollview .ReservedCols ()
177- d .scrollview .SetSize (regionWidth , visibleLines )
178-
179- // Set scrollview position for mouse hit-testing (auto-computed from dialog position)
180- // Y offset: border(1) + padding(1) + headerLines(3) = 5
181- dialogRow , dialogCol := d .Position ()
182- d .scrollview .SetPosition (dialogCol + 3 , dialogRow + 2 + headerLines )
183-
184- d .scrollview .SetContent (contentLines , len (contentLines ))
185-
186- scrollableContent := d .scrollview .View ()
187- parts := append (allLines [:headerLines ], scrollableContent )
188- parts = append (parts , "" , RenderHelpKeys (regionWidth , "↑↓" , "scroll" , "Esc" , "close" ))
189- return lipgloss .JoinVertical (lipgloss .Left , parts ... )
190- }
0 commit comments