| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- package dialog
- import (
- "fmt"
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/viewport"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
- "strings"
- )
- type PermissionAction string
- // Permission responses
- const (
- PermissionAllow PermissionAction = "allow"
- PermissionAllowForSession PermissionAction = "allow_session"
- PermissionDeny PermissionAction = "deny"
- )
- // PermissionResponseMsg represents the user's response to a permission request
- type PermissionResponseMsg struct {
- // Permission permission.PermissionRequest
- Action PermissionAction
- }
- // PermissionDialogCmp interface for permission dialog component
- type PermissionDialogCmp interface {
- tea.Model
- layout.Bindings
- // SetPermissions(permission permission.PermissionRequest) tea.Cmd
- }
- type permissionsMapping struct {
- Left key.Binding
- Right key.Binding
- EnterSpace key.Binding
- Allow key.Binding
- AllowSession key.Binding
- Deny key.Binding
- Tab key.Binding
- }
- var permissionsKeys = permissionsMapping{
- Left: key.NewBinding(
- key.WithKeys("left"),
- key.WithHelp("←", "switch options"),
- ),
- Right: key.NewBinding(
- key.WithKeys("right"),
- key.WithHelp("→", "switch options"),
- ),
- EnterSpace: key.NewBinding(
- key.WithKeys("enter", " "),
- key.WithHelp("enter/space", "confirm"),
- ),
- Allow: key.NewBinding(
- key.WithKeys("a"),
- key.WithHelp("a", "allow"),
- ),
- AllowSession: key.NewBinding(
- key.WithKeys("s"),
- key.WithHelp("s", "allow for session"),
- ),
- Deny: key.NewBinding(
- key.WithKeys("d"),
- key.WithHelp("d", "deny"),
- ),
- Tab: key.NewBinding(
- key.WithKeys("tab"),
- key.WithHelp("tab", "switch options"),
- ),
- }
- // permissionDialogCmp is the implementation of PermissionDialog
- type permissionDialogCmp struct {
- width int
- height int
- // permission permission.PermissionRequest
- windowSize tea.WindowSizeMsg
- contentViewPort viewport.Model
- selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
- diffCache map[string]string
- markdownCache map[string]string
- }
- func (p *permissionDialogCmp) Init() tea.Cmd {
- return p.contentViewPort.Init()
- }
- func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- p.windowSize = msg
- cmd := p.SetSize()
- cmds = append(cmds, cmd)
- p.markdownCache = make(map[string]string)
- p.diffCache = make(map[string]string)
- // case tea.KeyMsg:
- // switch {
- // case key.Matches(msg, permissionsKeys.Right) || key.Matches(msg, permissionsKeys.Tab):
- // p.selectedOption = (p.selectedOption + 1) % 3
- // return p, nil
- // case key.Matches(msg, permissionsKeys.Left):
- // p.selectedOption = (p.selectedOption + 2) % 3
- // case key.Matches(msg, permissionsKeys.EnterSpace):
- // return p, p.selectCurrentOption()
- // case key.Matches(msg, permissionsKeys.Allow):
- // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
- // case key.Matches(msg, permissionsKeys.AllowSession):
- // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
- // case key.Matches(msg, permissionsKeys.Deny):
- // return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
- // default:
- // // Pass other keys to viewport
- // viewPort, cmd := p.contentViewPort.Update(msg)
- // p.contentViewPort = viewPort
- // cmds = append(cmds, cmd)
- // }
- }
- return p, tea.Batch(cmds...)
- }
- func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
- var action PermissionAction
- switch p.selectedOption {
- case 0:
- action = PermissionAllow
- case 1:
- action = PermissionAllowForSession
- case 2:
- action = PermissionDeny
- }
- return util.CmdHandler(PermissionResponseMsg{Action: action}) // , Permission: p.permission})
- }
- func (p *permissionDialogCmp) renderButtons() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
- allowStyle := baseStyle
- allowSessionStyle := baseStyle
- denyStyle := baseStyle
- spacerStyle := baseStyle.Background(t.Background())
- // Style the selected button
- switch p.selectedOption {
- case 0:
- allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
- allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
- denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
- case 1:
- allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
- allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
- denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
- case 2:
- allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
- allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
- denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
- }
- allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
- allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
- denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
- content := lipgloss.JoinHorizontal(
- lipgloss.Left,
- allowButton,
- spacerStyle.Render(" "),
- allowSessionButton,
- spacerStyle.Render(" "),
- denyButton,
- spacerStyle.Render(" "),
- )
- remainingWidth := p.width - lipgloss.Width(content)
- if remainingWidth > 0 {
- content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
- }
- return content
- }
- func (p *permissionDialogCmp) renderHeader() string {
- return "NOT IMPLEMENTED"
- // t := theme.CurrentTheme()
- // baseStyle := styles.BaseStyle()
- //
- // toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
- // toolValue := baseStyle.
- // Foreground(t.Text()).
- // Width(p.width - lipgloss.Width(toolKey)).
- // Render(fmt.Sprintf(": %s", p.permission.ToolName))
- //
- // pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
- //
- // // Get the current working directory to display relative path
- // relativePath := p.permission.Path
- // if filepath.IsAbs(relativePath) {
- // if cwd, err := filepath.Rel(config.WorkingDirectory(), relativePath); err == nil {
- // relativePath = cwd
- // }
- // }
- //
- // pathValue := baseStyle.
- // Foreground(t.Text()).
- // Width(p.width - lipgloss.Width(pathKey)).
- // Render(fmt.Sprintf(": %s", relativePath))
- //
- // headerParts := []string{
- // lipgloss.JoinHorizontal(
- // lipgloss.Left,
- // toolKey,
- // toolValue,
- // ),
- // baseStyle.Render(strings.Repeat(" ", p.width)),
- // lipgloss.JoinHorizontal(
- // lipgloss.Left,
- // pathKey,
- // pathValue,
- // ),
- // baseStyle.Render(strings.Repeat(" ", p.width)),
- // }
- //
- // // Add tool-specific header information
- // switch p.permission.ToolName {
- // case "bash":
- // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
- // case "edit":
- // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
- // case "write":
- // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
- // case "fetch":
- // headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
- // }
- //
- // return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
- }
- func (p *permissionDialogCmp) renderBashContent() string {
- // t := theme.CurrentTheme()
- // baseStyle := styles.BaseStyle()
- //
- // if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
- // content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
- //
- // // Use the cache for markdown rendering
- // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
- // r := styles.GetMarkdownRenderer(p.width - 10)
- // s, err := r.Render(content)
- // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
- // })
- //
- // finalContent := baseStyle.
- // Width(p.contentViewPort.Width).
- // Render(renderedContent)
- // p.contentViewPort.SetContent(finalContent)
- // return p.styleViewport()
- // }
- return ""
- }
- func (p *permissionDialogCmp) renderEditContent() string {
- // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
- // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
- // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
- // })
- //
- // p.contentViewPort.SetContent(diff)
- // return p.styleViewport()
- // }
- return ""
- }
- func (p *permissionDialogCmp) renderPatchContent() string {
- // if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
- // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
- // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
- // })
- //
- // p.contentViewPort.SetContent(diff)
- // return p.styleViewport()
- // }
- return ""
- }
- func (p *permissionDialogCmp) renderWriteContent() string {
- // if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
- // // Use the cache for diff rendering
- // diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
- // return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
- // })
- //
- // p.contentViewPort.SetContent(diff)
- // return p.styleViewport()
- // }
- return ""
- }
- func (p *permissionDialogCmp) renderFetchContent() string {
- // t := theme.CurrentTheme()
- // baseStyle := styles.BaseStyle()
- //
- // if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
- // content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
- //
- // // Use the cache for markdown rendering
- // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
- // r := styles.GetMarkdownRenderer(p.width - 10)
- // s, err := r.Render(content)
- // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
- // })
- //
- // finalContent := baseStyle.
- // Width(p.contentViewPort.Width).
- // Render(renderedContent)
- // p.contentViewPort.SetContent(finalContent)
- // return p.styleViewport()
- // }
- return ""
- }
- func (p *permissionDialogCmp) renderDefaultContent() string {
- // t := theme.CurrentTheme()
- // baseStyle := styles.BaseStyle()
- //
- // content := p.permission.Description
- //
- // // Use the cache for markdown rendering
- // renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
- // r := styles.GetMarkdownRenderer(p.width - 10)
- // s, err := r.Render(content)
- // return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
- // })
- //
- // finalContent := baseStyle.
- // Width(p.contentViewPort.Width).
- // Render(renderedContent)
- // p.contentViewPort.SetContent(finalContent)
- //
- // if renderedContent == "" {
- // return ""
- // }
- //
- return p.styleViewport()
- }
- func (p *permissionDialogCmp) styleViewport() string {
- t := theme.CurrentTheme()
- contentStyle := lipgloss.NewStyle().
- Background(t.Background())
- return contentStyle.Render(p.contentViewPort.View())
- }
- func (p *permissionDialogCmp) render() string {
- return "NOT IMPLEMENTED"
- // t := theme.CurrentTheme()
- // baseStyle := styles.BaseStyle()
- //
- // title := baseStyle.
- // Bold(true).
- // Width(p.width - 4).
- // Foreground(t.Primary()).
- // Render("Permission Required")
- // // Render header
- // headerContent := p.renderHeader()
- // // Render buttons
- // buttons := p.renderButtons()
- //
- // // Calculate content height dynamically based on window size
- // p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title)
- // p.contentViewPort.Width = p.width - 4
- //
- // // Render content based on tool type
- // var contentFinal string
- // switch p.permission.ToolName {
- // case "bash":
- // contentFinal = p.renderBashContent()
- // case "edit":
- // contentFinal = p.renderEditContent()
- // case "patch":
- // contentFinal = p.renderPatchContent()
- // case "write":
- // contentFinal = p.renderWriteContent()
- // case "fetch":
- // contentFinal = p.renderFetchContent()
- // default:
- // contentFinal = p.renderDefaultContent()
- // }
- //
- // content := lipgloss.JoinVertical(
- // lipgloss.Top,
- // title,
- // baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
- // headerContent,
- // contentFinal,
- // buttons,
- // baseStyle.Render(strings.Repeat(" ", p.width-4)),
- // )
- //
- // return baseStyle.
- // Padding(1, 0, 0, 1).
- // Border(lipgloss.RoundedBorder()).
- // BorderBackground(t.Background()).
- // BorderForeground(t.TextMuted()).
- // Width(p.width).
- // Height(p.height).
- // Render(
- // content,
- // )
- }
- func (p *permissionDialogCmp) View() string {
- return p.render()
- }
- func (p *permissionDialogCmp) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(permissionsKeys)
- }
- func (p *permissionDialogCmp) SetSize() tea.Cmd {
- // if p.permission.ID == "" {
- // return nil
- // }
- // switch p.permission.ToolName {
- // case "bash":
- // p.width = int(float64(p.windowSize.Width) * 0.4)
- // p.height = int(float64(p.windowSize.Height) * 0.3)
- // case "edit":
- // p.width = int(float64(p.windowSize.Width) * 0.8)
- // p.height = int(float64(p.windowSize.Height) * 0.8)
- // case "write":
- // p.width = int(float64(p.windowSize.Width) * 0.8)
- // p.height = int(float64(p.windowSize.Height) * 0.8)
- // case "fetch":
- // p.width = int(float64(p.windowSize.Width) * 0.4)
- // p.height = int(float64(p.windowSize.Height) * 0.3)
- // default:
- // p.width = int(float64(p.windowSize.Width) * 0.7)
- // p.height = int(float64(p.windowSize.Height) * 0.5)
- // }
- return nil
- }
- // func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) tea.Cmd {
- // p.permission = permission
- // return p.SetSize()
- // }
- // Helper to get or set cached diff content
- func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
- if cached, ok := c.diffCache[key]; ok {
- return cached
- }
- content, err := generator()
- if err != nil {
- return fmt.Sprintf("Error formatting diff: %v", err)
- }
- c.diffCache[key] = content
- return content
- }
- // Helper to get or set cached markdown content
- func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
- if cached, ok := c.markdownCache[key]; ok {
- return cached
- }
- content, err := generator()
- if err != nil {
- return fmt.Sprintf("Error rendering markdown: %v", err)
- }
- c.markdownCache[key] = content
- return content
- }
- func NewPermissionDialogCmp() PermissionDialogCmp {
- // Create viewport for content
- contentViewport := viewport.New(0, 0)
- return &permissionDialogCmp{
- contentViewPort: contentViewport,
- selectedOption: 0, // Default to "Allow"
- diffCache: make(map[string]string),
- markdownCache: make(map[string]string),
- }
- }
|