fix(renderer): bound pane title cache
- commit
- fb92feb23eedfd63016272fd73db6417c25638d5
- author
- brice · · 2026-07-09 23:41 UTC
- committer
- brice · · 2026-07-10 05:40 UTC
- parent
- 77ab500
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +51 -0
M internal/usecase/daemon/pane_titles_test.go +1 -0
| @@ -60,6 +60,7 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) { | |||
| 60 | 60 | } | |
| 61 | 61 | ||
| 62 | 62 | title := d.refreshPaneTitle(sess, "pane-1") | |
| 63 | + | require.Equal(t, "vim", title) | |
| 63 | 64 | ||
| 64 | 65 | p := sess.activeTab().focusedPane() | |
| 65 | 66 | p.mu.Lock() | |
| @@ -60,6 +60,7 @@ func TestRefreshPaneTitleUsesForegroundProcessComm(t *testing.T) { | |||||
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| 62 | title := d.refreshPaneTitle(sess, "pane-1") | 62 | title := d.refreshPaneTitle(sess, "pane-1") | ||
| 63 | + | require.Equal(t, "vim", title) | |||
| 63 | 64 | ||||
| 64 | p := sess.activeTab().focusedPane() | 65 | p := sess.activeTab().focusedPane() | ||
| 65 | p.mu.Lock() | 66 | p.mu.Lock() | ||
M internal/usecase/daemon/render.go +13 -0
| @@ -649,6 +649,19 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||
| 649 | 649 | } | |
| 650 | 650 | placements = []layout.Placement{{ID: fallback.id, Content: contentArea}} | |
| 651 | 651 | } | |
| 652 | + | if titleGenerations != nil { | |
| 653 | + | currentTitleBars := make(map[layout.PaneID]struct{}, len(placements)) | |
| 654 | + | for _, pl := range placements { | |
| 655 | + | if pl.TitleBar.Height > 0 { | |
| 656 | + | currentTitleBars[pl.ID] = struct{}{} | |
| 657 | + | } | |
| 658 | + | } | |
| 659 | + | for id := range titleGenerations { | |
| 660 | + | if _, current := currentTitleBars[id]; !current { | |
| 661 | + | delete(titleGenerations, id) | |
| 662 | + | } | |
| 663 | + | } | |
| 664 | + | } | |
| 652 | 665 | if ok && !cacheValid { | |
| 653 | 666 | drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme)) | |
| 654 | 667 | } | |
| @@ -649,6 +649,19 @@ func composeTabFrameIntoWithLayoutOptions(tb *tab, frame renderer.Frame, area do | |||||
| 649 | } | 649 | } | ||
| 650 | placements = []layout.Placement{{ID: fallback.id, Content: contentArea}} | 650 | placements = []layout.Placement{{ID: fallback.id, Content: contentArea}} | ||
| 651 | } | 651 | } | ||
| 652 | + | if titleGenerations != nil { | |||
| 653 | + | currentTitleBars := make(map[layout.PaneID]struct{}, len(placements)) | |||
| 654 | + | for _, pl := range placements { | |||
| 655 | + | if pl.TitleBar.Height > 0 { | |||
| 656 | + | currentTitleBars[pl.ID] = struct{}{} | |||
| 657 | + | } | |||
| 658 | + | } | |||
| 659 | + | for id := range titleGenerations { | |||
| 660 | + | if _, current := currentTitleBars[id]; !current { | |||
| 661 | + | delete(titleGenerations, id) | |||
| 662 | + | } | |||
| 663 | + | } | |||
| 664 | + | } | |||
| 652 | if ok && !cacheValid { | 665 | if ok && !cacheValid { | ||
| 653 | drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme)) | 666 | drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme)) | ||
| 654 | } | 667 | } | ||
A internal/usecase/daemon/title_cache_regression_test.go +37 -0
| @@ -0,0 +1,37 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/stretchr/testify/require" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/vev/internal/domain" | |
| 10 | + | "github.com/bnema/vev/internal/usecase/layout" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | func TestComposeClientFrameCachePrunesTitleGenerationsAfterLayoutChurn(t *testing.T) { | |
| 14 | + | win := newTab(nil, domain.Size{Cols: 20, Rows: 5}) | |
| 15 | + | first := win.focusedPane() | |
| 16 | + | var bars barCache | |
| 17 | + | var composed composedFrameCache | |
| 18 | + | state := barState{status: (&session{id: "s", name: "work", tabs: []*tab{win}}).statusSegments()} | |
| 19 | + | ||
| 20 | + | for i := 0; i < 8; i++ { | |
| 21 | + | id := layout.PaneID(fmt.Sprintf("pane-%d", i+2)) | |
| 22 | + | replacement := newPane(id, nil, domain.Size{Cols: 20, Rows: 3}) | |
| 23 | + | win.panes = map[layout.PaneID]*pane{first.id: first, id: replacement} | |
| 24 | + | win.tree.Root = &layout.Node{Kind: layout.Stack, Children: []*layout.Node{ | |
| 25 | + | layout.NewLeaf(first.id), | |
| 26 | + | layout.NewLeaf(id), | |
| 27 | + | }, Expanded: id} | |
| 28 | + | win.tree.Focus = id | |
| 29 | + | ||
| 30 | + | composeClientFrameWithLayoutCached(state, win, i == 0, solveTabLayoutLocked(win), &bars, &composed) | |
| 31 | + | ||
| 32 | + | require.Equal(t, map[layout.PaneID]uint64{ | |
| 33 | + | first.id: composed.titleGenerations[first.id], | |
| 34 | + | id: composed.titleGenerations[id], | |
| 35 | + | }, composed.titleGenerations) | |
| 36 | + | } | |
| 37 | + | } | |
| @@ -0,0 +1,37 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/stretchr/testify/require" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/vev/internal/domain" | |||
| 10 | + | "github.com/bnema/vev/internal/usecase/layout" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | func TestComposeClientFrameCachePrunesTitleGenerationsAfterLayoutChurn(t *testing.T) { | |||
| 14 | + | win := newTab(nil, domain.Size{Cols: 20, Rows: 5}) | |||
| 15 | + | first := win.focusedPane() | |||
| 16 | + | var bars barCache | |||
| 17 | + | var composed composedFrameCache | |||
| 18 | + | state := barState{status: (&session{id: "s", name: "work", tabs: []*tab{win}}).statusSegments()} | |||
| 19 | + | ||||
| 20 | + | for i := 0; i < 8; i++ { | |||
| 21 | + | id := layout.PaneID(fmt.Sprintf("pane-%d", i+2)) | |||
| 22 | + | replacement := newPane(id, nil, domain.Size{Cols: 20, Rows: 3}) | |||
| 23 | + | win.panes = map[layout.PaneID]*pane{first.id: first, id: replacement} | |||
| 24 | + | win.tree.Root = &layout.Node{Kind: layout.Stack, Children: []*layout.Node{ | |||
| 25 | + | layout.NewLeaf(first.id), | |||
| 26 | + | layout.NewLeaf(id), | |||
| 27 | + | }, Expanded: id} | |||
| 28 | + | win.tree.Focus = id | |||
| 29 | + | ||||
| 30 | + | composeClientFrameWithLayoutCached(state, win, i == 0, solveTabLayoutLocked(win), &bars, &composed) | |||
| 31 | + | ||||
| 32 | + | require.Equal(t, map[layout.PaneID]uint64{ | |||
| 33 | + | first.id: composed.titleGenerations[first.id], | |||
| 34 | + | id: composed.titleGenerations[id], | |||
| 35 | + | }, composed.titleGenerations) | |||
| 36 | + | } | |||
| 37 | + | } | |||