reimplement agent,provider and add file history

This commit is contained in:
Kujtim Hoxha
2025-04-16 20:06:23 +02:00
parent 76b4065f17
commit bbfa60c787
73 changed files with 3742 additions and 4026 deletions

View File

@@ -12,12 +12,9 @@ import (
"github.com/kujtimiihoxha/termai/internal/diff"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
"github.com/kujtimiihoxha/termai/internal/permission"
"github.com/kujtimiihoxha/termai/internal/tui/components/core"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
"github.com/kujtimiihoxha/termai/internal/tui/util"
"github.com/charmbracelet/huh"
)
type PermissionAction string
@@ -35,69 +32,64 @@ type PermissionResponseMsg struct {
Action PermissionAction
}
// PermissionDialog interface for permission dialog component
type PermissionDialog interface {
// PermissionDialogCmp interface for permission dialog component
type PermissionDialogCmp interface {
tea.Model
layout.Sizeable
layout.Bindings
SetPermissions(permission permission.PermissionRequest)
}
type keyMap struct {
ChangeFocus key.Binding
type permissionsMapping struct {
LeftRight key.Binding
EnterSpace key.Binding
Allow key.Binding
AllowSession key.Binding
Deny key.Binding
Tab key.Binding
}
var keyMapValue = keyMap{
ChangeFocus: key.NewBinding(
var permissionsKeys = permissionsMapping{
LeftRight: key.NewBinding(
key.WithKeys("left", "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("A"),
key.WithHelp("A", "allow for session"),
),
Deny: key.NewBinding(
key.WithKeys("d"),
key.WithHelp("d", "deny"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "change focus"),
key.WithHelp("tab", "switch options"),
),
}
// permissionDialogCmp is the implementation of PermissionDialog
type permissionDialogCmp struct {
form *huh.Form
width int
height int
permission permission.PermissionRequest
windowSize tea.WindowSizeMsg
r *glamour.TermRenderer
contentViewPort viewport.Model
isViewportFocus bool
selectOption *huh.Select[string]
}
selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
// formatDiff formats a diff string with colors for additions and deletions
func formatDiff(diffText string) string {
lines := strings.Split(diffText, "\n")
var formattedLines []string
// Define styles for different line types
addStyle := lipgloss.NewStyle().Foreground(styles.Green)
removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
// Process each line
for _, line := range lines {
if strings.HasPrefix(line, "+") {
formattedLines = append(formattedLines, addStyle.Render(line))
} else if strings.HasPrefix(line, "-") {
formattedLines = append(formattedLines, removeStyle.Render(line))
} else if strings.HasPrefix(line, "Changes:") || strings.HasPrefix(line, " ...") {
formattedLines = append(formattedLines, headerStyle.Render(line))
} else if strings.HasPrefix(line, " ") {
formattedLines = append(formattedLines, contextStyle.Render(line))
} else {
formattedLines = append(formattedLines, line)
}
}
// Join all formatted lines
return strings.Join(formattedLines, "\n")
diffCache map[string]string
markdownCache map[string]string
}
func (p *permissionDialogCmp) Init() tea.Cmd {
return nil
return p.contentViewPort.Init()
}
func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -106,373 +98,363 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
p.windowSize = msg
p.SetSize()
p.markdownCache = make(map[string]string)
p.diffCache = make(map[string]string)
case tea.KeyMsg:
if key.Matches(msg, keyMapValue.ChangeFocus) {
p.isViewportFocus = !p.isViewportFocus
if p.isViewportFocus {
p.selectOption.Blur()
// Add a visual indicator for focus change
cmds = append(cmds, tea.Batch(
util.ReportInfo("Viewing content - use arrow keys to scroll"),
))
} else {
p.selectOption.Focus()
// Add a visual indicator for focus change
cmds = append(cmds, tea.Batch(
util.CmdHandler(util.ReportInfo("Select an action")),
))
}
return p, tea.Batch(cmds...)
}
}
if p.isViewportFocus {
viewPort, cmd := p.contentViewPort.Update(msg)
p.contentViewPort = viewPort
cmds = append(cmds, cmd)
} else {
form, cmd := p.form.Update(msg)
if f, ok := form.(*huh.Form); ok {
p.form = f
switch {
case key.Matches(msg, permissionsKeys.LeftRight) || key.Matches(msg, permissionsKeys.Tab):
// Change selected option
p.selectedOption = (p.selectedOption + 1) % 3
return p, nil
case key.Matches(msg, permissionsKeys.EnterSpace):
// Select current option
return p, p.selectCurrentOption()
case key.Matches(msg, permissionsKeys.Allow):
// Select Allow
return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
case key.Matches(msg, permissionsKeys.AllowSession):
// Select Allow for session
return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
case key.Matches(msg, permissionsKeys.Deny):
// Select 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)
}
if p.form.State == huh.StateCompleted {
// Get the selected action
action := p.form.GetString("action")
// Close the dialog and return the response
return p, tea.Batch(
util.CmdHandler(core.DialogCloseMsg{}),
util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}),
)
}
}
return p, tea.Batch(cmds...)
}
func (p *permissionDialogCmp) render() string {
keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater)
valueStyle := lipgloss.NewStyle().Foreground(styles.Peach)
func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
var action PermissionAction
form := p.form.View()
headerParts := []string{
lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)),
" ",
lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
" ",
switch p.selectedOption {
case 0:
action = PermissionAllow
case 1:
action = PermissionAllowForSession
case 2:
action = PermissionDeny
}
// Create the header content first so it can be used in all cases
headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
return util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission})
}
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
glamour.WithWordWrap(p.width-10),
glamour.WithEmoji(),
func (p *permissionDialogCmp) renderButtons() string {
allowStyle := styles.BaseStyle
allowSessionStyle := styles.BaseStyle
denyStyle := styles.BaseStyle
spacerStyle := styles.BaseStyle.Background(styles.Background)
// Style the selected button
switch p.selectedOption {
case 0:
allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
case 1:
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
case 2:
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
}
allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (A)")
denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
content := lipgloss.JoinHorizontal(
lipgloss.Left,
allowButton,
spacerStyle.Render(" "),
allowSessionButton,
spacerStyle.Render(" "),
denyButton,
spacerStyle.Render(" "),
)
// Handle different tool types
remainingWidth := p.width - lipgloss.Width(content)
if remainingWidth > 0 {
content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
}
return content
}
func (p *permissionDialogCmp) renderHeader() string {
toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
toolValue := styles.BaseStyle.
Foreground(styles.Forground).
Width(p.width - lipgloss.Width(toolKey)).
Render(fmt.Sprintf(": %s", p.permission.ToolName))
pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
pathValue := styles.BaseStyle.
Foreground(styles.Forground).
Width(p.width - lipgloss.Width(pathKey)).
Render(fmt.Sprintf(": %s", p.permission.Path))
headerParts := []string{
lipgloss.JoinHorizontal(
lipgloss.Left,
toolKey,
toolValue,
),
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
lipgloss.JoinHorizontal(
lipgloss.Left,
pathKey,
pathValue,
),
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
}
// Add tool-specific header information
switch p.permission.ToolName {
case tools.BashToolName:
pr := p.permission.Params.(tools.BashPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Command:"))
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command"))
case tools.EditToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
case tools.WriteToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
case tools.FetchToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL"))
}
return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
}
func (p *permissionDialogCmp) renderBashContent() string {
if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
renderedContent, _ := r.Render(content)
p.contentViewPort.Width = p.width - 2 - 2
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(true)),
glamour.WithWordWrap(p.width-10),
)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
})
// Calculate content height dynamically based on content
contentLines := len(strings.Split(renderedContent, "\n"))
// Set a reasonable min/max for the viewport height
minContentHeight := 3
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
// Add some padding to the content lines
contentHeight := contentLines + 2
contentHeight = max(contentHeight, minContentHeight)
contentHeight = min(contentHeight, maxContentHeight)
p.contentViewPort.Height = contentHeight
p.contentViewPort.SetContent(renderedContent)
// Style the viewport
var contentBorder lipgloss.Border
var borderColor lipgloss.TerminalColor
if p.isViewportFocus {
contentBorder = lipgloss.DoubleBorder()
borderColor = styles.Blue
} else {
contentBorder = lipgloss.RoundedBorder()
borderColor = styles.Flamingo
}
contentStyle := lipgloss.NewStyle().
MarginTop(1).
Padding(0, 1).
Border(contentBorder).
BorderForeground(borderColor)
if p.isViewportFocus {
contentStyle = contentStyle.BorderBackground(styles.Surface0)
}
contentFinal := contentStyle.Render(p.contentViewPort.View())
return lipgloss.JoinVertical(
lipgloss.Top,
headerContent,
contentFinal,
form,
)
case tools.EditToolName:
pr := p.permission.Params.(tools.EditPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Update"))
// Recreate header content with the updated headerParts
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
// Format the diff with colors
// Set up viewport for the diff content
p.contentViewPort.Width = p.width - 2 - 2
// Calculate content height dynamically based on window size
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
p.contentViewPort.Height = maxContentHeight
diff, err := diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
if err != nil {
diff = fmt.Sprintf("Error formatting diff: %v", err)
}
p.contentViewPort.SetContent(diff)
// Style the viewport
var contentBorder lipgloss.Border
var borderColor lipgloss.TerminalColor
if p.isViewportFocus {
contentBorder = lipgloss.DoubleBorder()
borderColor = styles.Blue
} else {
contentBorder = lipgloss.RoundedBorder()
borderColor = styles.Flamingo
}
contentStyle := lipgloss.NewStyle().
MarginTop(1).
Padding(0, 1).
Border(contentBorder).
BorderForeground(borderColor)
if p.isViewportFocus {
contentStyle = contentStyle.BorderBackground(styles.Surface0)
}
contentFinal := contentStyle.Render(p.contentViewPort.View())
return lipgloss.JoinVertical(
lipgloss.Top,
headerContent,
contentFinal,
form,
)
case tools.WriteToolName:
pr := p.permission.Params.(tools.WritePermissionsParams)
headerParts = append(headerParts, keyStyle.Render("Content"))
// Recreate header content with the updated headerParts
headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
// Set up viewport for the content
p.contentViewPort.Width = p.width - 2 - 2
// Calculate content height dynamically based on window size
maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
p.contentViewPort.Height = maxContentHeight
diff, err := diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
if err != nil {
diff = fmt.Sprintf("Error formatting diff: %v", err)
}
p.contentViewPort.SetContent(diff)
// Style the viewport
var contentBorder lipgloss.Border
var borderColor lipgloss.TerminalColor
if p.isViewportFocus {
contentBorder = lipgloss.DoubleBorder()
borderColor = styles.Blue
} else {
contentBorder = lipgloss.RoundedBorder()
borderColor = styles.Flamingo
}
contentStyle := lipgloss.NewStyle().
MarginTop(1).
Padding(0, 1).
Border(contentBorder).
BorderForeground(borderColor)
if p.isViewportFocus {
contentStyle = contentStyle.BorderBackground(styles.Surface0)
}
contentFinal := contentStyle.Render(p.contentViewPort.View())
return lipgloss.JoinVertical(
lipgloss.Top,
headerContent,
contentFinal,
form,
)
case tools.FetchToolName:
pr := p.permission.Params.(tools.FetchPermissionsParams)
headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
content := p.permission.Description
renderedContent, _ := r.Render(content)
p.contentViewPort.Width = p.width - 2 - 2
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
p.contentViewPort.SetContent(renderedContent)
// Style the viewport
contentStyle := lipgloss.NewStyle().
MarginTop(1).
Padding(0, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(styles.Flamingo)
contentFinal := contentStyle.Render(p.contentViewPort.View())
if renderedContent == "" {
contentFinal = ""
}
return lipgloss.JoinVertical(
lipgloss.Top,
headerContent,
contentFinal,
form,
)
default:
content := p.permission.Description
renderedContent, _ := r.Render(content)
p.contentViewPort.Width = p.width - 2 - 2
p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
p.contentViewPort.SetContent(renderedContent)
// Style the viewport
contentStyle := lipgloss.NewStyle().
MarginTop(1).
Padding(0, 1).
Border(lipgloss.RoundedBorder()).
BorderForeground(styles.Flamingo)
contentFinal := contentStyle.Render(p.contentViewPort.View())
if renderedContent == "" {
contentFinal = ""
}
return lipgloss.JoinVertical(
lipgloss.Top,
headerContent,
contentFinal,
form,
)
finalContent := styles.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) 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 {
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, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(true)),
glamour.WithWordWrap(p.width-10),
)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
})
p.contentViewPort.SetContent(renderedContent)
return p.styleViewport()
}
return ""
}
func (p *permissionDialogCmp) renderDefaultContent() string {
content := p.permission.Description
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
glamour.WithWordWrap(p.width-10),
)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
})
p.contentViewPort.SetContent(renderedContent)
if renderedContent == "" {
return ""
}
return p.styleViewport()
}
func (p *permissionDialogCmp) styleViewport() string {
contentStyle := lipgloss.NewStyle().
Background(styles.Background)
return contentStyle.Render(p.contentViewPort.View())
}
func (p *permissionDialogCmp) render() string {
title := styles.BaseStyle.
Bold(true).
Width(p.width - 4).
Foreground(styles.PrimaryColor).
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 tools.BashToolName:
contentFinal = p.renderBashContent()
case tools.EditToolName:
contentFinal = p.renderEditContent()
case tools.WriteToolName:
contentFinal = p.renderWriteContent()
case tools.FetchToolName:
contentFinal = p.renderFetchContent()
default:
contentFinal = p.renderDefaultContent()
}
content := lipgloss.JoinVertical(
lipgloss.Top,
title,
styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
headerContent,
contentFinal,
buttons,
)
return styles.BaseStyle.
Padding(1, 0, 0, 1).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
Width(p.width).
Height(p.height).
Render(
content,
)
}
func (p *permissionDialogCmp) View() string {
return p.render()
}
func (p *permissionDialogCmp) GetSize() (int, int) {
return p.width, p.height
}
func (p *permissionDialogCmp) SetSize(width int, height int) {
p.width = width
p.height = height
p.form = p.form.WithWidth(width)
}
func (p *permissionDialogCmp) BindingKeys() []key.Binding {
return p.form.KeyBinds()
return layout.KeyMapToSlice(helpKeys)
}
func newPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialog {
// Create a note field for displaying the content
func (p *permissionDialogCmp) SetSize() {
if p.permission.ID == "" {
return
}
switch p.permission.ToolName {
case tools.BashToolName:
p.width = int(float64(p.windowSize.Width) * 0.4)
p.height = int(float64(p.windowSize.Height) * 0.3)
case tools.EditToolName:
p.width = int(float64(p.windowSize.Width) * 0.8)
p.height = int(float64(p.windowSize.Height) * 0.8)
case tools.WriteToolName:
p.width = int(float64(p.windowSize.Width) * 0.8)
p.height = int(float64(p.windowSize.Height) * 0.8)
case tools.FetchToolName:
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)
}
}
// Create select field for the permission options
selectOption := huh.NewSelect[string]().
Key("action").
Options(
huh.NewOption("Allow", string(PermissionAllow)),
huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
huh.NewOption("Deny", string(PermissionDeny)),
).
Title("Select an action")
func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) {
p.permission = permission
p.SetSize()
}
// Apply theme
theme := styles.HuhTheme()
// 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
}
// Setup form width and height
form := huh.NewForm(huh.NewGroup(selectOption)).
WithShowHelp(false).
WithTheme(theme).
WithShowErrors(false)
content, err := generator()
if err != nil {
return fmt.Sprintf("Error formatting diff: %v", err)
}
// Focus the form for immediate interaction
selectOption.Focus()
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{
permission: permission,
form: form,
selectOption: selectOption,
contentViewPort: contentViewport,
selectedOption: 0, // Default to "Allow"
diffCache: make(map[string]string),
markdownCache: make(map[string]string),
}
}
// NewPermissionDialogCmd creates a new permission dialog command
func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
permDialog := newPermissionDialogCmp(permission)
// Create the dialog layout
dialogPane := layout.NewSinglePane(
permDialog.(*permissionDialogCmp),
layout.WithSinglePaneBordered(true),
layout.WithSinglePaneFocusable(true),
layout.WithSinglePaneActiveColor(styles.Warning),
layout.WithSinglePaneBorderText(map[layout.BorderPosition]string{
layout.TopMiddleBorder: " Permission Required ",
}),
)
// Focus the dialog
dialogPane.Focus()
widthRatio := 0.7
heightRatio := 0.6
minWidth := 100
minHeight := 30
// Make the dialog size more appropriate for different tools
switch permission.ToolName {
case tools.BashToolName:
// For bash commands, use a more compact dialog
widthRatio = 0.7
heightRatio = 0.4 // Reduced from 0.5
minWidth = 100
minHeight = 20 // Reduced from 30
}
// Return the dialog command
return util.CmdHandler(core.DialogMsg{
Content: dialogPane,
WidthRatio: widthRatio,
HeightRatio: heightRatio,
MinWidth: minWidth,
MinHeight: minHeight,
})
}