feat: add keybinding to permissions dialog to navigate the diffview

Andrey Nering created

Change summary

internal/tui/components/dialogs/permissions/keys.go        | 47 ++++++-
internal/tui/components/dialogs/permissions/permissions.go | 41 ++++++
2 files changed, 78 insertions(+), 10 deletions(-)

Detailed changes

internal/tui/components/dialogs/permissions/keys.go 🔗

@@ -11,7 +11,12 @@ type KeyMap struct {
 	Select,
 	Allow,
 	AllowSession,
-	Deny key.Binding
+	Deny,
+	ToggleDiffMode,
+	ScrollDown,
+	ScrollUp key.Binding
+	ScrollLeft,
+	ScrollRight key.Binding
 }
 
 func DefaultKeyMap() KeyMap {
@@ -41,9 +46,29 @@ func DefaultKeyMap() KeyMap {
 			key.WithHelp("d", "deny"),
 		),
 		Select: key.NewBinding(
-			key.WithKeys("enter", "tab", "ctrl+y"),
+			key.WithKeys("enter", "ctrl+y"),
 			key.WithHelp("enter", "confirm"),
 		),
+		ToggleDiffMode: key.NewBinding(
+			key.WithKeys("t"),
+			key.WithHelp("t", "toggle diff mode"),
+		),
+		ScrollDown: key.NewBinding(
+			key.WithKeys("shift+down", "J"),
+			key.WithHelp("shift+↓", "scroll down"),
+		),
+		ScrollUp: key.NewBinding(
+			key.WithKeys("shift+up", "K"),
+			key.WithHelp("shift+↑", "scroll up"),
+		),
+		ScrollLeft: key.NewBinding(
+			key.WithKeys("shift+left", "H"),
+			key.WithHelp("shift+←", "scroll left"),
+		),
+		ScrollRight: key.NewBinding(
+			key.WithKeys("shift+right", "L"),
+			key.WithHelp("shift+→", "scroll right"),
+		),
 	}
 }
 
@@ -57,6 +82,11 @@ func (k KeyMap) KeyBindings() []key.Binding {
 		k.Allow,
 		k.AllowSession,
 		k.Deny,
+		k.ToggleDiffMode,
+		k.ScrollDown,
+		k.ScrollUp,
+		k.ScrollLeft,
+		k.ScrollRight,
 	}
 }
 
@@ -74,9 +104,14 @@ func (k KeyMap) FullHelp() [][]key.Binding {
 // ShortHelp implements help.KeyMap.
 func (k KeyMap) ShortHelp() []key.Binding {
 	return []key.Binding{
-		k.Allow,
-		k.AllowSession,
-		k.Deny,
-		k.Select,
+		k.ToggleDiffMode,
+		key.NewBinding(
+			key.WithKeys("shift+left", "shift+down", "shift+up", "shift+right"),
+			key.WithHelp("shift+←↓↑→", "scroll"),
+		),
+		key.NewBinding(
+			key.WithKeys("shift+h", "shift+j", "shift+k", "shift+l"),
+			key.WithHelp("shift+hjkl", "scroll"),
+		),
 	}
 }

internal/tui/components/dialogs/permissions/permissions.go 🔗

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/charmbracelet/bubbles/v2/help"
 	"github.com/charmbracelet/bubbles/v2/key"
 	"github.com/charmbracelet/bubbles/v2/viewport"
 	tea "github.com/charmbracelet/bubbletea/v2"
@@ -50,6 +51,11 @@ type permissionDialogCmp struct {
 	contentViewPort viewport.Model
 	selectedOption  int // 0: Allow, 1: Allow for session, 2: Deny
 
+	// Diff view state
+	diffSplitMode bool // true for split, false for unified
+	diffXOffset   int  // horizontal scroll offset
+	diffYOffset   int  // vertical scroll offset
+
 	keyMap KeyMap
 }
 
@@ -101,6 +107,21 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				util.CmdHandler(dialogs.CloseDialogMsg{}),
 				util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}),
 			)
+		case key.Matches(msg, p.keyMap.ToggleDiffMode):
+			p.diffSplitMode = !p.diffSplitMode
+			return p, nil
+		case key.Matches(msg, p.keyMap.ScrollDown):
+			p.diffYOffset += 1
+			return p, nil
+		case key.Matches(msg, p.keyMap.ScrollUp):
+			p.diffYOffset = max(0, p.diffYOffset-1)
+			return p, nil
+		case key.Matches(msg, p.keyMap.ScrollLeft):
+			p.diffXOffset = max(0, p.diffXOffset-5)
+			return p, nil
+		case key.Matches(msg, p.keyMap.ScrollRight):
+			p.diffXOffset += 5
+			return p, nil
 		default:
 			// Pass other keys to viewport
 			viewPort, cmd := p.contentViewPort.Update(msg)
@@ -269,8 +290,15 @@ func (p *permissionDialogCmp) renderEditContent() string {
 		formatter := core.DiffFormatter().
 			Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
 			After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
+			Height(p.contentViewPort.Height()).
 			Width(p.contentViewPort.Width()).
-			Split()
+			XOffset(p.diffXOffset).
+			YOffset(p.diffYOffset)
+		if p.diffSplitMode {
+			formatter = formatter.Split()
+		} else {
+			formatter = formatter.Unified()
+		}
 
 		diff := formatter.String()
 		contentHeight := min(p.height-9, lipgloss.Height(diff))
@@ -367,11 +395,13 @@ func (p *permissionDialogCmp) render() string {
 
 	// Render content based on tool type
 	var contentFinal string
+	var contentHelp string
 	switch p.permission.ToolName {
 	case tools.BashToolName:
 		contentFinal = p.renderBashContent()
 	case tools.EditToolName:
 		contentFinal = p.renderEditContent()
+		contentHelp = help.New().View(p.keyMap)
 	case tools.WriteToolName:
 		contentFinal = p.renderWriteContent()
 	case tools.FetchToolName:
@@ -381,8 +411,7 @@ func (p *permissionDialogCmp) render() string {
 	}
 	// Calculate content height dynamically based on window size
 
-	content := lipgloss.JoinVertical(
-		lipgloss.Top,
+	strs := []string{
 		title,
 		"",
 		headerContent,
@@ -390,7 +419,11 @@ func (p *permissionDialogCmp) render() string {
 		"",
 		buttons,
 		"",
-	)
+	}
+	if contentHelp != "" {
+		strs = append(strs, "", contentHelp)
+	}
+	content := lipgloss.JoinVertical(lipgloss.Top, strs...)
 
 	return baseStyle.
 		Padding(0, 1).