on main help sign in
perf: attempt to improve permissions dialog performance
ab7c7e6Andrey Nering
internal/tui/components/dialogs/permissions/permissions.go+114−55
@@ -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
58
58
59+ // Caching
60+ cachedContent string
61+ cachedContentKey string
62+ contentDirty bool
63+
59 keyMap KeyMap
64 keyMap KeyMap
60}
65}
61
66
@@ -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
70 }
76 }
71}
77}
72
78
@@ -74,6 +80,10 @@ func (p *permissionDialogCmp) Init() tea.Cmd {
74 return p.contentViewPort.Init()
80 return p.contentViewPort.Init()
75}
81}
76
82
83+func (p *permissionDialogCmp) supportsDiffView() bool {
84+ return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName
85+}
86+
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
79
89
@@ -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}),
109 )
120 )
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
126+ }
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
132+ }
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
138+ }
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
144+ }
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
150+ }
125 default:
151 default:
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...))
248}
274}
249
275
250−func (p *permissionDialogCmp) renderBashContent() string {
276+func (p *permissionDialogCmp) generateContentKey() string {
277+ return fmt.Sprintf("%s_%s_%t_%d_%d_%d_%d",
278+ p.permission.ID,
279+ p.permission.ToolName,
280+ p.diffSplitMode,
281+ p.diffXOffset,
282+ p.diffYOffset,
283+ p.contentViewPort.Width(),
284+ p.contentViewPort.Height())
285+}
286+
287+func (p *permissionDialogCmp) getOrGenerateContent() string {
288+ currentKey := p.generateContentKey()
289+
290+ // Return cached content if available and not dirty
291+ if !p.contentDirty && p.cachedContentKey == currentKey && p.cachedContent != "" {
292+ return p.cachedContent
293+ }
294+
295+ // Generate new content
296+ var content string
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()
306+ default:
307+ content = p.generateDefaultContent()
308+ }
309+
310+ // Cache the result
311+ p.cachedContent = content
312+ p.cachedContentKey = currentKey
313+ p.contentDirty = false
314+
315+ return content
316+}
317+
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)
279
347
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()
284 }
349 }
285 return ""
350 return ""
286}
351}
287
352
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 {
301 }
366 }
302
367
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
308 }
370 }
309 return ""
371 return ""
310}
372}
311
373
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()
386+ } else {
387+ formatter = formatter.Unified()
388+ }
320
389
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
326 }
392 }
327 return ""
393 return ""
328}
394}
329
395
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)
346
412
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()
351 }
414 }
352 return ""
415 return ""
353}
416}
354
417
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)
358
421
@@ -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)
372
434
373 if renderedContent == "" {
435 if renderedContent == "" {
374 return ""
436 return ""
375 }
437 }
376
438
377− return p.styleViewport()
439+ return finalContent
378}
440}
379
441
380func (p *permissionDialogCmp) styleViewport() string {
442func (p *permissionDialogCmp) styleViewport() string {
@@ -393,21 +455,17 @@ func (p *permissionDialogCmp) render() string {
393
455
394 p.contentViewPort.SetWidth(p.width - 4)
456 p.contentViewPort.SetWidth(p.width - 4)
395
457
396− // Render content based on tool type
458+ // Get cached or generate content
397− var contentFinal string
459+ contentFinal := p.getOrGenerateContent()
460+
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)
465+
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()
409− default:
410− contentFinal = p.renderDefaultContent()
411 }
469 }
412 // Calculate content height dynamically based on window size
470 // Calculate content height dynamically based on window size
413
471
@@ -415,7 +473,7 @@ func (p *permissionDialogCmp) render() string {
415 title,
473 title,
416 "",
474 "",
417 headerContent,
475 headerContent,
418− contentFinal,
476+ p.styleViewport(),
419 "",
477 "",
420 buttons,
478 buttons,
421 "",
479 "",
@@ -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
445 }
503 }
504+
505+ oldWidth, oldHeight := p.width, p.height
506+
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)
462 }
523 }
463− return nil
464−}
465
524
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)
470 }
528 }
529+
471− return content
530+ return nil
472}
531}
473
532
474func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
533func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {