detached fb92feb

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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) {
6060 }
6161
6262 title := d.refreshPaneTitle(sess, "pane-1")
63+ require.Equal(t, "vim", title)
6364
6465 p := sess.activeTab().focusedPane()
6566 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
649649 }
650650 placements = []layout.Placement{{ID: fallback.id, Content: contentArea}}
651651 }
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+ }
652665 if ok && !cacheValid {
653666 drawDividers(frame, root, area, themeui.DimStyle(newThemeStyles(theme).border, theme))
654667 }
@@ -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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close