@@ -56,6 +56,11 @@ type permissionDialogCmp struct {
56 diffXOffset int // horizontal scroll offset
56 diffXOffset int // horizontal scroll offset
57 diffYOffset int // vertical scroll offset
57 diffYOffset int // vertical scroll offset
61+ cachedContentKey string
59 keyMap KeyMap
64 keyMap KeyMap
@@ -67,6 +72,7 @@ func NewPermissionDialogCmp(permission permission.PermissionRequest) PermissionD
67 selectedOption: 0, // Default to "Allow"
72 selectedOption: 0, // Default to "Allow"
68 permission: permission,
73 permission: permission,
69 keyMap: DefaultKeyMap(),
74 keyMap: DefaultKeyMap(),
75+ contentDirty: true, // Mark as dirty initially
@@ -74,6 +80,10 @@ func (p *permissionDialogCmp) Init() tea.Cmd {
74 return p.contentViewPort.Init()
80 return p.contentViewPort.Init()
83+func (p *permissionDialogCmp) supportsDiffView() bool {
84+ return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName
77func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
87func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
78 var cmds []tea.Cmd
88 var cmds []tea.Cmd
@@ -81,6 +91,7 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81 case tea.WindowSizeMsg:
91 case tea.WindowSizeMsg:
82 p.wWidth = msg.Width
92 p.wWidth = msg.Width
83 p.wHeight = msg.Height
93 p.wHeight = msg.Height
94+ p.contentDirty = true // Mark content as dirty on window resize
84 cmd := p.SetSize()
95 cmd := p.SetSize()
85 cmds = append(cmds, cmd)
96 cmds = append(cmds, cmd)
86 case tea.KeyPressMsg:
97 case tea.KeyPressMsg:
@@ -108,20 +119,35 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
108 util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}),
119 util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}),
110 case key.Matches(msg, p.keyMap.ToggleDiffMode):
121 case key.Matches(msg, p.keyMap.ToggleDiffMode):
122+ if p.supportsDiffView() {
111− p.diffSplitMode = !p.diffSplitMode
123+ p.diffSplitMode = !p.diffSplitMode
124+ p.contentDirty = true // Mark content as dirty when diff mode changes
112− return p, nil
125+ return p, nil
113 case key.Matches(msg, p.keyMap.ScrollDown):
127 case key.Matches(msg, p.keyMap.ScrollDown):
128+ if p.supportsDiffView() {
114− p.diffYOffset += 1
129+ p.diffYOffset += 1
130+ p.contentDirty = true // Mark content as dirty when scrolling
115− return p, nil
131+ return p, nil
116 case key.Matches(msg, p.keyMap.ScrollUp):
133 case key.Matches(msg, p.keyMap.ScrollUp):
134+ if p.supportsDiffView() {
117− p.diffYOffset = max(0, p.diffYOffset-1)
135+ p.diffYOffset = max(0, p.diffYOffset-1)
136+ p.contentDirty = true // Mark content as dirty when scrolling
118− return p, nil
137+ return p, nil
119 case key.Matches(msg, p.keyMap.ScrollLeft):
139 case key.Matches(msg, p.keyMap.ScrollLeft):
140+ if p.supportsDiffView() {
120− p.diffXOffset = max(0, p.diffXOffset-5)
141+ p.diffXOffset = max(0, p.diffXOffset-5)
142+ p.contentDirty = true // Mark content as dirty when scrolling
121− return p, nil
143+ return p, nil
122 case key.Matches(msg, p.keyMap.ScrollRight):
145 case key.Matches(msg, p.keyMap.ScrollRight):
146+ if p.supportsDiffView() {
123− p.diffXOffset += 5
147+ p.diffXOffset += 5
148+ p.contentDirty = true // Mark content as dirty when scrolling
124− return p, nil
149+ return p, nil
126 // Pass other keys to viewport
152 // Pass other keys to viewport
127 viewPort, cmd := p.contentViewPort.Update(msg)
153 viewPort, cmd := p.contentViewPort.Update(msg)
@@ -247,7 +273,49 @@ func (p *permissionDialogCmp) renderHeader() string {
247 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
273 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
250−func (p *permissionDialogCmp) renderBashContent() string {
276+func (p *permissionDialogCmp) generateContentKey() string {
277+ return fmt.Sprintf("%s_%s_%t_%d_%d_%d_%d",
279+ p.permission.ToolName,
283+ p.contentViewPort.Width(),
284+ p.contentViewPort.Height())
287+func (p *permissionDialogCmp) getOrGenerateContent() string {
288+ currentKey := p.generateContentKey()
290+ // Return cached content if available and not dirty
291+ if !p.contentDirty && p.cachedContentKey == currentKey && p.cachedContent != "" {
292+ return p.cachedContent
295+ // Generate new content
297+ switch p.permission.ToolName {
298+ case tools.BashToolName:
299+ content = p.generateBashContent()
300+ case tools.EditToolName:
301+ content = p.generateEditContent()
302+ case tools.WriteToolName:
303+ content = p.generateWriteContent()
304+ case tools.FetchToolName:
305+ content = p.generateFetchContent()
307+ content = p.generateDefaultContent()
311+ p.cachedContent = content
312+ p.cachedContentKey = currentKey
313+ p.contentDirty = false
318+func (p *permissionDialogCmp) generateBashContent() string {
251 t := styles.CurrentTheme()
319 t := styles.CurrentTheme()
252 baseStyle := t.S().Base.Background(t.BgSubtle)
320 baseStyle := t.S().Base.Background(t.BgSubtle)
253 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
321 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
@@ -277,15 +345,12 @@ func (p *permissionDialogCmp) renderBashContent() string {
277 Width(p.contentViewPort.Width()).
345 Width(p.contentViewPort.Width()).
278 Render(renderedContent)
346 Render(renderedContent)
280− contentHeight := min(p.height-9, lipgloss.Height(finalContent))
348+ return finalContent
281− p.contentViewPort.SetHeight(contentHeight)
282− p.contentViewPort.SetContent(finalContent)
283− return p.styleViewport()
285 return ""
350 return ""
288−func (p *permissionDialogCmp) renderEditContent() string {
353+func (p *permissionDialogCmp) generateEditContent() string {
289 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
354 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
290 formatter := core.DiffFormatter().
355 formatter := core.DiffFormatter().
291 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
356 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
@@ -301,33 +366,34 @@ func (p *permissionDialogCmp) renderEditContent() string {
303 diff := formatter.String()
368 diff := formatter.String()
304− contentHeight := min(p.height-9, lipgloss.Height(diff))
305− p.contentViewPort.SetHeight(contentHeight)
306− p.contentViewPort.SetContent(diff)
307− return p.styleViewport()
369+ return diff
309 return ""
371 return ""
312−func (p *permissionDialogCmp) renderWriteContent() string {
374+func (p *permissionDialogCmp) generateWriteContent() string {
313 if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
375 if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
314 // Use the cache for diff rendering
376 // Use the cache for diff rendering
315 formatter := core.DiffFormatter().
377 formatter := core.DiffFormatter().
316 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
378 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
317 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
379 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
380+ Height(p.contentViewPort.Height()).
318 Width(p.contentViewPort.Width()).
381 Width(p.contentViewPort.Width()).
319− Split()
382+ XOffset(p.diffXOffset).
383+ YOffset(p.diffYOffset)
384+ if p.diffSplitMode {
385+ formatter = formatter.Split()
387+ formatter = formatter.Unified()
321 diff := formatter.String()
390 diff := formatter.String()
322− contentHeight := min(p.height-9, lipgloss.Height(diff))
323− p.contentViewPort.SetHeight(contentHeight)
324− p.contentViewPort.SetContent(diff)
325− return p.styleViewport()
391+ return diff
327 return ""
393 return ""
330−func (p *permissionDialogCmp) renderFetchContent() string {
396+func (p *permissionDialogCmp) generateFetchContent() string {
331 t := styles.CurrentTheme()
397 t := styles.CurrentTheme()
332 baseStyle := t.S().Base.Background(t.BgSubtle)
398 baseStyle := t.S().Base.Background(t.BgSubtle)
333 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
399 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
@@ -344,15 +410,12 @@ func (p *permissionDialogCmp) renderFetchContent() string {
344 Width(p.contentViewPort.Width()).
410 Width(p.contentViewPort.Width()).
345 Render(renderedContent)
411 Render(renderedContent)
347− contentHeight := min(p.height-9, lipgloss.Height(finalContent))
413+ return finalContent
348− p.contentViewPort.SetHeight(contentHeight)
349− p.contentViewPort.SetContent(finalContent)
350− return p.styleViewport()
352 return ""
415 return ""
355−func (p *permissionDialogCmp) renderDefaultContent() string {
418+func (p *permissionDialogCmp) generateDefaultContent() string {
356 t := styles.CurrentTheme()
419 t := styles.CurrentTheme()
357 baseStyle := t.S().Base.Background(t.BgSubtle)
420 baseStyle := t.S().Base.Background(t.BgSubtle)
@@ -368,13 +431,12 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
368 finalContent := baseStyle.
431 finalContent := baseStyle.
369 Width(p.contentViewPort.Width()).
432 Width(p.contentViewPort.Width()).
370 Render(renderedContent)
433 Render(renderedContent)
371− p.contentViewPort.SetContent(finalContent)
373 if renderedContent == "" {
435 if renderedContent == "" {
374 return ""
436 return ""
377− return p.styleViewport()
439+ return finalContent
380func (p *permissionDialogCmp) styleViewport() string {
442func (p *permissionDialogCmp) styleViewport() string {
@@ -393,21 +455,17 @@ func (p *permissionDialogCmp) render() string {
394 p.contentViewPort.SetWidth(p.width - 4)
456 p.contentViewPort.SetWidth(p.width - 4)
396− // Render content based on tool type
458+ // Get cached or generate content
397− var contentFinal string
459+ contentFinal := p.getOrGenerateContent()
461+ // Always set viewport content (the caching is handled in getOrGenerateContent)
462+ contentHeight := min(p.height-9, lipgloss.Height(contentFinal))
463+ p.contentViewPort.SetHeight(contentHeight)
464+ p.contentViewPort.SetContent(contentFinal)
398 var contentHelp string
466 var contentHelp string
399− switch p.permission.ToolName {
467+ if p.supportsDiffView() {
400− case tools.BashToolName:
401− contentFinal = p.renderBashContent()
402− case tools.EditToolName:
403− contentFinal = p.renderEditContent()
404 contentHelp = help.New().View(p.keyMap)
468 contentHelp = help.New().View(p.keyMap)
405− case tools.WriteToolName:
406− contentFinal = p.renderWriteContent()
407− case tools.FetchToolName:
408− contentFinal = p.renderFetchContent()
410− contentFinal = p.renderDefaultContent()
412 // Calculate content height dynamically based on window size
470 // Calculate content height dynamically based on window size
@@ -415,7 +473,7 @@ func (p *permissionDialogCmp) render() string {
417 headerContent,
475 headerContent,
418− contentFinal,
476+ p.styleViewport(),
@@ -443,6 +501,9 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
443 if p.permission.ID == "" {
501 if p.permission.ID == "" {
444 return nil
502 return nil
505+ oldWidth, oldHeight := p.width, p.height
446 switch p.permission.ToolName {
507 switch p.permission.ToolName {
447 case tools.BashToolName:
508 case tools.BashToolName:
448 p.width = int(float64(p.wWidth) * 0.4)
509 p.width = int(float64(p.wWidth) * 0.4)
@@ -460,15 +521,13 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
460 p.width = int(float64(p.wWidth) * 0.7)
521 p.width = int(float64(p.wWidth) * 0.7)
461 p.height = int(float64(p.wHeight) * 0.5)
522 p.height = int(float64(p.wHeight) * 0.5)
466−func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
525+ // Mark content as dirty if size changed
467− content, err := generator()
526+ if oldWidth != p.width || oldHeight != p.height {
468− if err != nil {
527+ p.contentDirty = true
469− return fmt.Sprintf("Error formatting diff: %v", err)
471− return content
530+ return nil
474func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
533func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {