detached a27f761

refactor(daemon): separate recent route presentation

commit
a27f761df844312b86e8a6a7476f0184e6be762a
author
brice · · 2026-08-09 11:09 UTC
parent
b9f0565
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +341 -70
M internal/usecase/daemon/floating_render_test.go +1 -1
@@ -749,7 +749,7 @@ func benchmarkCapturedRenderState() capturedRenderState {
749749 status: statusSnapshot{session: "main", tabs: []statusTab{{name: "main", paneTitle: "shell", active: true}}},
750750 topRight: "vev",
751751 bottomRight: "main",
752- mru: []recentSession{{name: "work"}, {name: "logs"}},
752+ mru: []recentRouteDisplay{{name: "work"}, {name: "logs"}},
753753 theme: terminalTheme,
754754 },
755755 theme: terminalTheme,
@@ -749,7 +749,7 @@ func benchmarkCapturedRenderState() capturedRenderState {
749 status: statusSnapshot{session: "main", tabs: []statusTab{{name: "main", paneTitle: "shell", active: true}}},749 status: statusSnapshot{session: "main", tabs: []statusTab{{name: "main", paneTitle: "shell", active: true}}},
750 topRight: "vev",750 topRight: "vev",
751 bottomRight: "main",751 bottomRight: "main",
752- mru: []recentSession{{name: "work"}, {name: "logs"}},752+ mru: []recentRouteDisplay{{name: "work"}, {name: "logs"}},
753 theme: terminalTheme,753 theme: terminalTheme,
754 },754 },
755 theme: terminalTheme,755 theme: terminalTheme,
M internal/usecase/daemon/palette.go +3 -2
@@ -94,8 +94,9 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command) []
9494 }
9595
9696 func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints {
97- names := make([]string, len(recent))
98- for i, entry := range recent {
97+ formatted := formatRecentRoutePresentations(recentRoutePresentations(recent))
98+ names := make([]string, len(formatted))
99+ for i, entry := range formatted {
99100 names[i] = entry.name
100101 }
101102 return palette.BuildRecentSessionHints(names, args)
@@ -94,8 +94,9 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command) []
94 }94 }
95 95
96 func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints {96 func recentSessionHints(recent []recentSession, args []string) palette.ContextualHints {
97- names := make([]string, len(recent))97+ formatted := formatRecentRoutePresentations(recentRoutePresentations(recent))
98- for i, entry := range recent {98+ names := make([]string, len(formatted))
99+ for i, entry := range formatted {
99 names[i] = entry.name100 names[i] = entry.name
100 }101 }
101 return palette.BuildRecentSessionHints(names, args)102 return palette.BuildRecentSessionHints(names, args)
A internal/usecase/daemon/recent_route_presentation.go +98 -0
@@ -0,0 +1,98 @@
1+package daemon
2+
3+// recentRouteKind identifies the origin represented by a presentation value.
4+// It is intentionally daemon-local in this layer; the client route protocol
5+// and its wire kind are introduced by the next stacked branch.
6+type recentRouteKind uint8
7+
8+const (
9+ recentRouteLocal recentRouteKind = iota
10+ recentRouteRemote
11+)
12+
13+// recentRoutePresentation is the compact immutable value shared by collection,
14+// formatting, and the future client-published snapshot. It contains only
15+// display data; it never carries session ownership, selection identity,
16+// transport capabilities, credentials, or attach targets.
17+type recentRoutePresentation struct {
18+ name string
19+ hostLabel string
20+ kind recentRouteKind
21+ ephemeral bool
22+ attention bool
23+}
24+
25+// recentRouteDisplay is a formatted, render-ready copy of a presentation. The
26+// source value is never mutated while duplicate names are qualified.
27+type recentRouteDisplay struct {
28+ name string
29+ kind recentRouteKind
30+ ephemeral bool
31+ attention bool
32+}
33+
34+// routePresentation returns the local daemon's display-only view of a
35+// selection value. Selection identity remains in recentSession for palette
36+// execution and is not passed into rendering.
37+func (entry recentSession) routePresentation() recentRoutePresentation {
38+ return recentRoutePresentation{
39+ name: entry.name,
40+ kind: recentRouteLocal,
41+ attention: entry.attention,
42+ }
43+}
44+
45+func recentRoutePresentations(entries []recentSession) []recentRoutePresentation {
46+ if entries == nil {
47+ return nil
48+ }
49+ out := make([]recentRoutePresentation, len(entries))
50+ for i, entry := range entries {
51+ out[i] = entry.routePresentation()
52+ }
53+ return out
54+}
55+
56+// formatRecentRoutePresentations is the single label formatter used by the
57+// normal MRU bar and ranked recent-session palette rendering. It returns an
58+// owned slice so callers can retain the result without consulting live state.
59+func formatRecentRoutePresentations(entries []recentRoutePresentation) []recentRouteDisplay {
60+ if entries == nil {
61+ return nil
62+ }
63+ nameCounts := make(map[string]int, len(entries))
64+ for _, entry := range entries {
65+ nameCounts[entry.name]++
66+ }
67+
68+ out := make([]recentRouteDisplay, len(entries))
69+ for i, entry := range entries {
70+ out[i] = recentRouteDisplay{
71+ name: formatRecentRouteName(entry, nameCounts[entry.name] > 1),
72+ kind: entry.kind,
73+ ephemeral: entry.ephemeral,
74+ attention: entry.attention,
75+ }
76+ }
77+ return out
78+}
79+
80+func formatRecentRouteName(entry recentRoutePresentation, ambiguous bool) string {
81+ name := entry.name
82+ if entry.ephemeral {
83+ name += "*"
84+ }
85+ if entry.kind != recentRouteRemote && !ambiguous {
86+ return name
87+ }
88+
89+ host := entry.hostLabel
90+ if host == "" {
91+ if entry.kind == recentRouteRemote {
92+ host = "remote"
93+ } else {
94+ host = "local"
95+ }
96+ }
97+ return name + "@" + host
98+}
@@ -0,0 +1,98 @@
1+package daemon
2+
3+// recentRouteKind identifies the origin represented by a presentation value.
4+// It is intentionally daemon-local in this layer; the client route protocol
5+// and its wire kind are introduced by the next stacked branch.
6+type recentRouteKind uint8
7+
8+const (
9+ recentRouteLocal recentRouteKind = iota
10+ recentRouteRemote
11+)
12+
13+// recentRoutePresentation is the compact immutable value shared by collection,
14+// formatting, and the future client-published snapshot. It contains only
15+// display data; it never carries session ownership, selection identity,
16+// transport capabilities, credentials, or attach targets.
17+type recentRoutePresentation struct {
18+ name string
19+ hostLabel string
20+ kind recentRouteKind
21+ ephemeral bool
22+ attention bool
23+}
24+
25+// recentRouteDisplay is a formatted, render-ready copy of a presentation. The
26+// source value is never mutated while duplicate names are qualified.
27+type recentRouteDisplay struct {
28+ name string
29+ kind recentRouteKind
30+ ephemeral bool
31+ attention bool
32+}
33+
34+// routePresentation returns the local daemon's display-only view of a
35+// selection value. Selection identity remains in recentSession for palette
36+// execution and is not passed into rendering.
37+func (entry recentSession) routePresentation() recentRoutePresentation {
38+ return recentRoutePresentation{
39+ name: entry.name,
40+ kind: recentRouteLocal,
41+ attention: entry.attention,
42+ }
43+}
44+
45+func recentRoutePresentations(entries []recentSession) []recentRoutePresentation {
46+ if entries == nil {
47+ return nil
48+ }
49+ out := make([]recentRoutePresentation, len(entries))
50+ for i, entry := range entries {
51+ out[i] = entry.routePresentation()
52+ }
53+ return out
54+}
55+
56+// formatRecentRoutePresentations is the single label formatter used by the
57+// normal MRU bar and ranked recent-session palette rendering. It returns an
58+// owned slice so callers can retain the result without consulting live state.
59+func formatRecentRoutePresentations(entries []recentRoutePresentation) []recentRouteDisplay {
60+ if entries == nil {
61+ return nil
62+ }
63+ nameCounts := make(map[string]int, len(entries))
64+ for _, entry := range entries {
65+ nameCounts[entry.name]++
66+ }
67+
68+ out := make([]recentRouteDisplay, len(entries))
69+ for i, entry := range entries {
70+ out[i] = recentRouteDisplay{
71+ name: formatRecentRouteName(entry, nameCounts[entry.name] > 1),
72+ kind: entry.kind,
73+ ephemeral: entry.ephemeral,
74+ attention: entry.attention,
75+ }
76+ }
77+ return out
78+}
79+
80+func formatRecentRouteName(entry recentRoutePresentation, ambiguous bool) string {
81+ name := entry.name
82+ if entry.ephemeral {
83+ name += "*"
84+ }
85+ if entry.kind != recentRouteRemote && !ambiguous {
86+ return name
87+ }
88+
89+ host := entry.hostLabel
90+ if host == "" {
91+ if entry.kind == recentRouteRemote {
92+ host = "remote"
93+ } else {
94+ host = "local"
95+ }
96+ }
97+ return name + "@" + host
98+}
A internal/usecase/daemon/recent_route_presentation_test.go +163 -0
@@ -0,0 +1,163 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/internal/usecase/command"
7+ "github.com/bnema/vev/internal/usecase/palette"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestFormatRecentRoutePresentations(t *testing.T) {
12+ tests := []struct {
13+ name string
14+ input []recentRoutePresentation
15+ want []recentRouteDisplay
16+ }{
17+ {
18+ name: "unique local name stays compact",
19+ input: []recentRoutePresentation{{
20+ name: "editor", kind: recentRouteLocal,
21+ }},
22+ want: []recentRouteDisplay{{
23+ name: "editor", kind: recentRouteLocal,
24+ }},
25+ },
26+ {
27+ name: "remote name is host qualified",
28+ input: []recentRoutePresentation{{
29+ name: "logs", hostLabel: "edge", kind: recentRouteRemote,
30+ }},
31+ want: []recentRouteDisplay{{
32+ name: "logs@edge", kind: recentRouteRemote,
33+ }},
34+ },
35+ {
36+ name: "duplicate local and remote names qualify every entry",
37+ input: []recentRoutePresentation{
38+ {name: "logs", kind: recentRouteLocal},
39+ {name: "logs", hostLabel: "edge", kind: recentRouteRemote},
40+ },
41+ want: []recentRouteDisplay{
42+ {name: "logs@local", kind: recentRouteLocal},
43+ {name: "logs@edge", kind: recentRouteRemote},
44+ },
45+ },
46+ {
47+ name: "missing host uses kind fallback",
48+ input: []recentRoutePresentation{{
49+ name: "logs", kind: recentRouteRemote,
50+ }},
51+ want: []recentRouteDisplay{{
52+ name: "logs@remote", kind: recentRouteRemote,
53+ }},
54+ },
55+ {
56+ name: "authoritative markers survive formatting",
57+ input: []recentRoutePresentation{{
58+ name: "scratch", kind: recentRouteLocal, ephemeral: true, attention: true,
59+ }},
60+ want: []recentRouteDisplay{{
61+ name: "scratch*", kind: recentRouteLocal, ephemeral: true, attention: true,
62+ }},
63+ },
64+ }
65+
66+ for _, tt := range tests {
67+ t.Run(tt.name, func(t *testing.T) {
68+ input := append([]recentRoutePresentation(nil), tt.input...)
69+ got := formatRecentRoutePresentations(input)
70+
71+ require.Equal(t, tt.want, got)
72+ require.Equal(t, input, tt.input, "formatting must not mutate the source snapshot")
73+ })
74+ }
75+}
76+
77+func TestRecentRoutePresentationsProjectSelectionValues(t *testing.T) {
78+ entries := []recentSession{{id: "session-secret", name: "work", attention: true, mruAt: 17}}
79+
80+ got := recentRoutePresentations(entries)
81+
82+ require.Equal(t, []recentRoutePresentation{{
83+ name: "work", kind: recentRouteLocal, attention: true,
84+ }}, got)
85+}
86+
87+func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T) {
88+ recent := []recentSession{
89+ {name: "logs", attention: true},
90+ {name: "logs"},
91+ }
92+
93+ hints := recentSessionHints(recent, nil)
94+ require.Equal(t, []palette.RecentSessionHint{
95+ {Rank: 1, Name: "logs@local"},
96+ {Rank: 2, Name: "logs@local"},
97+ }, hints.Recent)
98+
99+ hints = palette.ContextualHints{
100+ Kind: command.ContextHintRecentSessions,
101+ SelectedRank: 9,
102+ Recent: []palette.RecentSessionHint{
103+ {Rank: 4, Name: "stale-label"},
104+ {Rank: 9, Name: "stale-label"},
105+ },
106+ }
107+ ranked := rankedRecentForHints(&hints, recent)
108+
109+ require.Equal(t, []rankedRecent{
110+ {rank: 4, name: "logs@local", kind: recentRouteLocal, attention: true},
111+ {rank: 9, name: "logs@local", kind: recentRouteLocal, selected: true},
112+ }, ranked)
113+}
114+
115+func TestFitRecentRouteEntriesKeepsWholeWideAndAttentionEntries(t *testing.T) {
116+ tests := []struct {
117+ name string
118+ entries []recentRouteDisplay
119+ width int
120+ want []string
121+ }{
122+ {
123+ name: "wide rune consumes its full cell width",
124+ entries: []recentRouteDisplay{
125+ {name: "界"},
126+ {name: "b"},
127+ },
128+ width: 12,
129+ want: []string{"界"},
130+ },
131+ {
132+ name: "attention bell is part of the entry width",
133+ entries: []recentRouteDisplay{
134+ {name: "a", attention: true},
135+ {name: "b"},
136+ },
137+ width: 12,
138+ want: []string{"a"},
139+ },
140+ }
141+
142+ for _, tt := range tests {
143+ t.Run(tt.name, func(t *testing.T) {
144+ got := fitMRU(tt.entries, tt.width, 0, "copy")
145+ names := make([]string, len(got))
146+ for i, entry := range got {
147+ names[i] = entry.name
148+ }
149+ require.Equal(t, tt.want, names)
150+ })
151+ }
152+}
153+
154+func TestRankedRecentKeepsCapturedRanksWhenFitting(t *testing.T) {
155+ entries := []rankedRecent{
156+ {rank: 3, name: "wide界"},
157+ {rank: 7, name: "next"},
158+ }
159+
160+ got := fitRankedRecent(entries, 17, 0, "")
161+
162+ require.Equal(t, []rankedRecent{{rank: 3, name: "wide界"}}, got)
163+}
@@ -0,0 +1,163 @@
1+package daemon
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/vev/internal/usecase/command"
7+ "github.com/bnema/vev/internal/usecase/palette"
8+ "github.com/stretchr/testify/require"
9+)
10+
11+func TestFormatRecentRoutePresentations(t *testing.T) {
12+ tests := []struct {
13+ name string
14+ input []recentRoutePresentation
15+ want []recentRouteDisplay
16+ }{
17+ {
18+ name: "unique local name stays compact",
19+ input: []recentRoutePresentation{{
20+ name: "editor", kind: recentRouteLocal,
21+ }},
22+ want: []recentRouteDisplay{{
23+ name: "editor", kind: recentRouteLocal,
24+ }},
25+ },
26+ {
27+ name: "remote name is host qualified",
28+ input: []recentRoutePresentation{{
29+ name: "logs", hostLabel: "edge", kind: recentRouteRemote,
30+ }},
31+ want: []recentRouteDisplay{{
32+ name: "logs@edge", kind: recentRouteRemote,
33+ }},
34+ },
35+ {
36+ name: "duplicate local and remote names qualify every entry",
37+ input: []recentRoutePresentation{
38+ {name: "logs", kind: recentRouteLocal},
39+ {name: "logs", hostLabel: "edge", kind: recentRouteRemote},
40+ },
41+ want: []recentRouteDisplay{
42+ {name: "logs@local", kind: recentRouteLocal},
43+ {name: "logs@edge", kind: recentRouteRemote},
44+ },
45+ },
46+ {
47+ name: "missing host uses kind fallback",
48+ input: []recentRoutePresentation{{
49+ name: "logs", kind: recentRouteRemote,
50+ }},
51+ want: []recentRouteDisplay{{
52+ name: "logs@remote", kind: recentRouteRemote,
53+ }},
54+ },
55+ {
56+ name: "authoritative markers survive formatting",
57+ input: []recentRoutePresentation{{
58+ name: "scratch", kind: recentRouteLocal, ephemeral: true, attention: true,
59+ }},
60+ want: []recentRouteDisplay{{
61+ name: "scratch*", kind: recentRouteLocal, ephemeral: true, attention: true,
62+ }},
63+ },
64+ }
65+
66+ for _, tt := range tests {
67+ t.Run(tt.name, func(t *testing.T) {
68+ input := append([]recentRoutePresentation(nil), tt.input...)
69+ got := formatRecentRoutePresentations(input)
70+
71+ require.Equal(t, tt.want, got)
72+ require.Equal(t, input, tt.input, "formatting must not mutate the source snapshot")
73+ })
74+ }
75+}
76+
77+func TestRecentRoutePresentationsProjectSelectionValues(t *testing.T) {
78+ entries := []recentSession{{id: "session-secret", name: "work", attention: true, mruAt: 17}}
79+
80+ got := recentRoutePresentations(entries)
81+
82+ require.Equal(t, []recentRoutePresentation{{
83+ name: "work", kind: recentRouteLocal, attention: true,
84+ }}, got)
85+}
86+
87+func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T) {
88+ recent := []recentSession{
89+ {name: "logs", attention: true},
90+ {name: "logs"},
91+ }
92+
93+ hints := recentSessionHints(recent, nil)
94+ require.Equal(t, []palette.RecentSessionHint{
95+ {Rank: 1, Name: "logs@local"},
96+ {Rank: 2, Name: "logs@local"},
97+ }, hints.Recent)
98+
99+ hints = palette.ContextualHints{
100+ Kind: command.ContextHintRecentSessions,
101+ SelectedRank: 9,
102+ Recent: []palette.RecentSessionHint{
103+ {Rank: 4, Name: "stale-label"},
104+ {Rank: 9, Name: "stale-label"},
105+ },
106+ }
107+ ranked := rankedRecentForHints(&hints, recent)
108+
109+ require.Equal(t, []rankedRecent{
110+ {rank: 4, name: "logs@local", kind: recentRouteLocal, attention: true},
111+ {rank: 9, name: "logs@local", kind: recentRouteLocal, selected: true},
112+ }, ranked)
113+}
114+
115+func TestFitRecentRouteEntriesKeepsWholeWideAndAttentionEntries(t *testing.T) {
116+ tests := []struct {
117+ name string
118+ entries []recentRouteDisplay
119+ width int
120+ want []string
121+ }{
122+ {
123+ name: "wide rune consumes its full cell width",
124+ entries: []recentRouteDisplay{
125+ {name: "界"},
126+ {name: "b"},
127+ },
128+ width: 12,
129+ want: []string{"界"},
130+ },
131+ {
132+ name: "attention bell is part of the entry width",
133+ entries: []recentRouteDisplay{
134+ {name: "a", attention: true},
135+ {name: "b"},
136+ },
137+ width: 12,
138+ want: []string{"a"},
139+ },
140+ }
141+
142+ for _, tt := range tests {
143+ t.Run(tt.name, func(t *testing.T) {
144+ got := fitMRU(tt.entries, tt.width, 0, "copy")
145+ names := make([]string, len(got))
146+ for i, entry := range got {
147+ names[i] = entry.name
148+ }
149+ require.Equal(t, tt.want, names)
150+ })
151+ }
152+}
153+
154+func TestRankedRecentKeepsCapturedRanksWhenFitting(t *testing.T) {
155+ entries := []rankedRecent{
156+ {rank: 3, name: "wide界"},
157+ {rank: 7, name: "next"},
158+ }
159+
160+ got := fitRankedRecent(entries, 17, 0, "")
161+
162+ require.Equal(t, []rankedRecent{{rank: 3, name: "wide界"}}, got)
163+}
M internal/usecase/daemon/recent_sessions.go +4 -2
@@ -6,8 +6,10 @@ import (
66 "github.com/bnema/vev/internal/domain"
77 )
88
9-// recentSession is an immutable, value-only description of a session for one
10-// interaction. It deliberately does not retain a live session pointer.
9+// recentSession is the immutable selection snapshot for one interaction. Its
10+// identity and MRU sequence are retained only for daemon-side execution and
11+// ordering; render paths project it into recentRoutePresentation, which has no
12+// selection identity or lifecycle authority.
1113 type recentSession struct {
1214 id domain.SessionID
1315 name string
@@ -6,8 +6,10 @@ import (
6 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7 )7 )
8 8
9-// recentSession is an immutable, value-only description of a session for one9+// recentSession is the immutable selection snapshot for one interaction. Its
10-// interaction. It deliberately does not retain a live session pointer.10+// identity and MRU sequence are retained only for daemon-side execution and
11+// ordering; render paths project it into recentRoutePresentation, which has no
12+// selection identity or lifecycle authority.
11 type recentSession struct {13 type recentSession struct {
12 id domain.SessionID14 id domain.SessionID
13 name string15 name string
M internal/usecase/daemon/render_capture.go +1 -1
@@ -28,7 +28,7 @@ type renderCaptureScratch struct {
2828 layoutFingerprint string
2929 layoutValid bool
3030 statusTabs []statusTab
31- mru []recentSession
31+ mru []recentRouteDisplay
3232 ranked []rankedRecent
3333 titleIDs []layout.PaneID
3434 receipts []damageReceipt
@@ -28,7 +28,7 @@ type renderCaptureScratch struct {
28 layoutFingerprint string28 layoutFingerprint string
29 layoutValid bool29 layoutValid bool
30 statusTabs []statusTab30 statusTabs []statusTab
31- mru []recentSession31+ mru []recentRouteDisplay
32 ranked []rankedRecent32 ranked []rankedRecent
33 titleIDs []layout.PaneID33 titleIDs []layout.PaneID
34 receipts []damageReceipt34 receipts []damageReceipt
M internal/usecase/daemon/status.go +55 -48
@@ -87,7 +87,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styl
8787 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
8888 for i, sess := range fittedMRU {
8989 style := styles.MRUStyle(i, len(fittedMRU))
90- drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)
90+ drawStatusSessionEntry(row, &x, sess, style, state.attentionFrame)
9191 }
9292 }
9393 drawRightPlainText(row, rightText, x, styles.SurfaceBar)
@@ -96,6 +96,8 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styl
9696 type rankedRecent struct {
9797 rank int
9898 name string
99+ kind recentRouteKind
100+ ephemeral bool
99101 attention bool
100102 selected bool
101103 }
@@ -112,9 +114,9 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
112114 writeStatusText(row, x, " ", style)
113115 }
114116
115-func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, attention bool, style renderer.Style, attentionFrame int) {
116- writeStatusText(row, x, " "+name, style)
117- if attention {
117+func drawStatusSessionEntry(row []renderer.Cell, x *int, entry recentRouteDisplay, style renderer.Style, attentionFrame int) {
118+ writeStatusText(row, x, " "+entry.name, style)
119+ if entry.attention {
118120 writeStatusText(row, x, " ", style)
119121 writeBell(row, x, attentionFrame, style)
120122 }
@@ -160,7 +162,7 @@ type barState struct {
160162 topRight string
161163 bottomRight string
162164 statusFeedback string
163- mru []recentSession
165+ mru []recentRouteDisplay
164166 rankedRecent []rankedRecent
165167 attentionFrame int
166168 // theme is the client's terminal theme, if reported. Its zero value
@@ -226,15 +228,25 @@ func rankedRecentForHints(hints *palette.ContextualHints, recent []recentSession
226228 if hints == nil || hints.Kind != command.ContextHintRecentSessions {
227229 return nil
228230 }
231+ formatted := formatRecentRoutePresentations(recentRoutePresentations(recent))
229232 entries := make([]rankedRecent, 0, len(hints.Recent))
230233 for i, hint := range hints.Recent {
231- entry := rankedRecent{rank: hint.Rank, name: hint.Name, selected: hint.Rank == hints.SelectedRank}
234+ display := recentRouteDisplay{name: hint.Name}
232235 // recent was copied with the hint under paletteMu, so this only enriches
233- // the render snapshot and never performs a live domain lookup.
234- if i < len(recent) {
235- entry.attention = recent[i].attention
236+ // the render snapshot and never performs a live domain lookup. The
237+ // formatted capture is authoritative when available, keeping ranked
238+ // JRS labels identical to normal MRU labels.
239+ if i < len(formatted) {
240+ display = formatted[i]
236241 }
237- entries = append(entries, entry)
242+ entries = append(entries, rankedRecent{
243+ rank: hint.Rank,
244+ name: display.name,
245+ kind: display.kind,
246+ ephemeral: display.ephemeral,
247+ attention: display.attention,
248+ selected: hint.Rank == hints.SelectedRank,
249+ })
238250 }
239251 return entries
240252 }
@@ -276,14 +288,14 @@ func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attached
276288 if ranked != nil {
277289 state.rankedRecent = ranked
278290 } else if d != nil {
279- state.mru = d.recentSessions(cur)
291+ state.mru = formatRecentRoutePresentations(recentRoutePresentations(d.recentSessions(cur)))
280292 }
281293 return state
282294 }
283295
284296 func (d *Daemon) barStateFor(cur *session, statusFeedback string) barState {
285297 state := d.barStateBase(cur, statusFeedback)
286- state.mru = d.recentSessions(cur)
298+ state.mru = formatRecentRoutePresentations(recentRoutePresentations(d.recentSessions(cur)))
287299 return state
288300 }
289301
@@ -383,33 +395,45 @@ func fitTabLabels(tabs []statusTab, rowLen int, rightText string) []fittedTabLab
383395 return labels
384396 }
385397
386-func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback string) []rankedRecent {
387- reserve := 1
388- if feedback != "" {
389- reserve = statusTextWidth(feedback) + 2
398+func recentRouteDisplayWidth(entry recentRouteDisplay) int {
399+ width := 2 + statusTextWidth(entry.name)
400+ if entry.attention {
401+ width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
390402 }
391- budget := rowLen - leftUsed - reserve
392- if budget <= 0 {
403+ return width
404+}
405+
406+func fitWholeRecentEntries[T any](entries []T, budget int, cost func(T) int) []T {
407+ if budget <= 0 || len(entries) == 0 {
393408 return nil
394409 }
395- cost := func(e rankedRecent) int {
396- width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(e.name)
397- if e.attention {
398- width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
399- }
400- return width
401- }
402410 used := 0
403411 for i, entry := range entries {
404- if used+cost(entry) > budget {
405- return entries[:i] // whole entries only; ranks remain their original MRU ranks.
406- }
407412 used += cost(entry)
413+ if used > budget {
414+ return entries[:i]
415+ }
408416 }
409417 return entries
410418 }
411419
412-func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []recentSession {
420+func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback string) []rankedRecent {
421+ reserve := 1
422+ if feedback != "" {
423+ reserve = statusTextWidth(feedback) + 2
424+ }
425+ budget := rowLen - leftUsed - reserve
426+ return fitWholeRecentEntries(entries, budget, func(entry rankedRecent) int {
427+ return recentRouteDisplayWidth(recentRouteDisplay{
428+ name: entry.name,
429+ kind: entry.kind,
430+ ephemeral: entry.ephemeral,
431+ attention: entry.attention,
432+ }) + statusTextWidth(strconv.Itoa(entry.rank)+":")
433+ })
434+}
435+
436+func fitMRU(entries []recentRouteDisplay, rowLen, leftUsed int, feedback string) []recentRouteDisplay {
413437 // With no feedback, keep one blank trailing cell; with feedback, reserve
414438 // its " text" width plus a one-cell gap so drawRightPlainText always fits.
415439 copyReserve := 1
@@ -420,34 +444,17 @@ func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []re
420444 if physicalBudget <= 0 || len(entries) == 0 {
421445 return nil
422446 }
423- cost := func(e recentSession) int {
424- n := 2 + statusTextWidth(e.name)
425- if e.attention {
426- n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
427- }
428- return n
429- }
430447
431448 budget := physicalBudget
432449 if feedback == "" {
433450 budget -= mruFutureRightReserve(rowLen)
434451 // Keep at least one recent session when it physically fits; the reserved
435452 // right side is only a budget preference, not a reason to hide all recents.
436- if firstCost := cost(entries[0]); firstCost <= physicalBudget && budget < firstCost {
453+ if firstCost := recentRouteDisplayWidth(entries[0]); firstCost <= physicalBudget && budget < firstCost {
437454 budget = firstCost
438455 }
439456 }
440- if budget <= 0 {
441- return nil
442- }
443- used := 0
444- for i, e := range entries {
445- used += cost(e)
446- if used > budget {
447- return entries[:i]
448- }
449- }
450- return entries
457+ return fitWholeRecentEntries(entries, budget, recentRouteDisplayWidth)
451458 }
452459
453460 const (
@@ -87,7 +87,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styl
87 fittedMRU := fitMRU(state.mru, len(row), x, rightText)87 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
88 for i, sess := range fittedMRU {88 for i, sess := range fittedMRU {
89 style := styles.MRUStyle(i, len(fittedMRU))89 style := styles.MRUStyle(i, len(fittedMRU))
90- drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)90+ drawStatusSessionEntry(row, &x, sess, style, state.attentionFrame)
91 }91 }
92 }92 }
93 drawRightPlainText(row, rightText, x, styles.SurfaceBar)93 drawRightPlainText(row, rightText, x, styles.SurfaceBar)
@@ -96,6 +96,8 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeui.Styl
96 type rankedRecent struct {96 type rankedRecent struct {
97 rank int97 rank int
98 name string98 name string
99+ kind recentRouteKind
100+ ephemeral bool
99 attention bool101 attention bool
100 selected bool102 selected bool
101 }103 }
@@ -112,9 +114,9 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
112 writeStatusText(row, x, " ", style)114 writeStatusText(row, x, " ", style)
113 }115 }
114 116
115-func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, attention bool, style renderer.Style, attentionFrame int) {117+func drawStatusSessionEntry(row []renderer.Cell, x *int, entry recentRouteDisplay, style renderer.Style, attentionFrame int) {
116- writeStatusText(row, x, " "+name, style)118+ writeStatusText(row, x, " "+entry.name, style)
117- if attention {119+ if entry.attention {
118 writeStatusText(row, x, " ", style)120 writeStatusText(row, x, " ", style)
119 writeBell(row, x, attentionFrame, style)121 writeBell(row, x, attentionFrame, style)
120 }122 }
@@ -160,7 +162,7 @@ type barState struct {
160 topRight string162 topRight string
161 bottomRight string163 bottomRight string
162 statusFeedback string164 statusFeedback string
163- mru []recentSession165+ mru []recentRouteDisplay
164 rankedRecent []rankedRecent166 rankedRecent []rankedRecent
165 attentionFrame int167 attentionFrame int
166 // theme is the client's terminal theme, if reported. Its zero value168 // theme is the client's terminal theme, if reported. Its zero value
@@ -226,15 +228,25 @@ func rankedRecentForHints(hints *palette.ContextualHints, recent []recentSession
226 if hints == nil || hints.Kind != command.ContextHintRecentSessions {228 if hints == nil || hints.Kind != command.ContextHintRecentSessions {
227 return nil229 return nil
228 }230 }
231+ formatted := formatRecentRoutePresentations(recentRoutePresentations(recent))
229 entries := make([]rankedRecent, 0, len(hints.Recent))232 entries := make([]rankedRecent, 0, len(hints.Recent))
230 for i, hint := range hints.Recent {233 for i, hint := range hints.Recent {
231- entry := rankedRecent{rank: hint.Rank, name: hint.Name, selected: hint.Rank == hints.SelectedRank}234+ display := recentRouteDisplay{name: hint.Name}
232 // recent was copied with the hint under paletteMu, so this only enriches235 // recent was copied with the hint under paletteMu, so this only enriches
233- // the render snapshot and never performs a live domain lookup.236+ // the render snapshot and never performs a live domain lookup. The
234- if i < len(recent) {237+ // formatted capture is authoritative when available, keeping ranked
235- entry.attention = recent[i].attention238+ // JRS labels identical to normal MRU labels.
239+ if i < len(formatted) {
240+ display = formatted[i]
236 }241 }
237- entries = append(entries, entry)242+ entries = append(entries, rankedRecent{
243+ rank: hint.Rank,
244+ name: display.name,
245+ kind: display.kind,
246+ ephemeral: display.ephemeral,
247+ attention: display.attention,
248+ selected: hint.Rank == hints.SelectedRank,
249+ })
238 }250 }
239 return entries251 return entries
240 }252 }
@@ -276,14 +288,14 @@ func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attached
276 if ranked != nil {288 if ranked != nil {
277 state.rankedRecent = ranked289 state.rankedRecent = ranked
278 } else if d != nil {290 } else if d != nil {
279- state.mru = d.recentSessions(cur)291+ state.mru = formatRecentRoutePresentations(recentRoutePresentations(d.recentSessions(cur)))
280 }292 }
281 return state293 return state
282 }294 }
283 295
284 func (d *Daemon) barStateFor(cur *session, statusFeedback string) barState {296 func (d *Daemon) barStateFor(cur *session, statusFeedback string) barState {
285 state := d.barStateBase(cur, statusFeedback)297 state := d.barStateBase(cur, statusFeedback)
286- state.mru = d.recentSessions(cur)298+ state.mru = formatRecentRoutePresentations(recentRoutePresentations(d.recentSessions(cur)))
287 return state299 return state
288 }300 }
289 301
@@ -383,33 +395,45 @@ func fitTabLabels(tabs []statusTab, rowLen int, rightText string) []fittedTabLab
383 return labels395 return labels
384 }396 }
385 397
386-func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback string) []rankedRecent {398+func recentRouteDisplayWidth(entry recentRouteDisplay) int {
387- reserve := 1399+ width := 2 + statusTextWidth(entry.name)
388- if feedback != "" {400+ if entry.attention {
389- reserve = statusTextWidth(feedback) + 2401+ width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
390 }402 }
391- budget := rowLen - leftUsed - reserve403+ return width
392- if budget <= 0 {404+}
405+
406+func fitWholeRecentEntries[T any](entries []T, budget int, cost func(T) int) []T {
407+ if budget <= 0 || len(entries) == 0 {
393 return nil408 return nil
394 }409 }
395- cost := func(e rankedRecent) int {
396- width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(e.name)
397- if e.attention {
398- width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
399- }
400- return width
401- }
402 used := 0410 used := 0
403 for i, entry := range entries {411 for i, entry := range entries {
404- if used+cost(entry) > budget {
405- return entries[:i] // whole entries only; ranks remain their original MRU ranks.
406- }
407 used += cost(entry)412 used += cost(entry)
413+ if used > budget {
414+ return entries[:i]
415+ }
408 }416 }
409 return entries417 return entries
410 }418 }
411 419
412-func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []recentSession {420+func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback string) []rankedRecent {
421+ reserve := 1
422+ if feedback != "" {
423+ reserve = statusTextWidth(feedback) + 2
424+ }
425+ budget := rowLen - leftUsed - reserve
426+ return fitWholeRecentEntries(entries, budget, func(entry rankedRecent) int {
427+ return recentRouteDisplayWidth(recentRouteDisplay{
428+ name: entry.name,
429+ kind: entry.kind,
430+ ephemeral: entry.ephemeral,
431+ attention: entry.attention,
432+ }) + statusTextWidth(strconv.Itoa(entry.rank)+":")
433+ })
434+}
435+
436+func fitMRU(entries []recentRouteDisplay, rowLen, leftUsed int, feedback string) []recentRouteDisplay {
413 // With no feedback, keep one blank trailing cell; with feedback, reserve437 // With no feedback, keep one blank trailing cell; with feedback, reserve
414 // its " text" width plus a one-cell gap so drawRightPlainText always fits.438 // its " text" width plus a one-cell gap so drawRightPlainText always fits.
415 copyReserve := 1439 copyReserve := 1
@@ -420,34 +444,17 @@ func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []re
420 if physicalBudget <= 0 || len(entries) == 0 {444 if physicalBudget <= 0 || len(entries) == 0 {
421 return nil445 return nil
422 }446 }
423- cost := func(e recentSession) int {
424- n := 2 + statusTextWidth(e.name)
425- if e.attention {
426- n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
427- }
428- return n
429- }
430 447
431 budget := physicalBudget448 budget := physicalBudget
432 if feedback == "" {449 if feedback == "" {
433 budget -= mruFutureRightReserve(rowLen)450 budget -= mruFutureRightReserve(rowLen)
434 // Keep at least one recent session when it physically fits; the reserved451 // Keep at least one recent session when it physically fits; the reserved
435 // right side is only a budget preference, not a reason to hide all recents.452 // right side is only a budget preference, not a reason to hide all recents.
436- if firstCost := cost(entries[0]); firstCost <= physicalBudget && budget < firstCost {453+ if firstCost := recentRouteDisplayWidth(entries[0]); firstCost <= physicalBudget && budget < firstCost {
437 budget = firstCost454 budget = firstCost
438 }455 }
439 }456 }
440- if budget <= 0 {457+ return fitWholeRecentEntries(entries, budget, recentRouteDisplayWidth)
441- return nil
442- }
443- used := 0
444- for i, e := range entries {
445- used += cost(e)
446- if used > budget {
447- return entries[:i]
448- }
449- }
450- return entries
451 }458 }
452 459
453 const (460 const (
M internal/usecase/daemon/status_test.go +16 -16
@@ -1111,7 +1111,7 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
11111111 drawStatusBarState(row, state.bars, resolveStyles(nil))
11121112 return rowText(row)
11131113 }
1114- normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "vty"}, {name: "misc"}}}
1114+ normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentRouteDisplay{{name: "vty"}, {name: "misc"}}}
11151115 ac.sendMu.Lock()
11161116 defer ac.sendMu.Unlock()
11171117
@@ -1147,7 +1147,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
11471147 state := barState{
11481148 status: statusSnapshot{session: "cur"},
11491149 attentionFrame: 1,
1150- mru: []recentSession{
1150+ mru: []recentRouteDisplay{
11511151 {name: "fresh"},
11521152 {name: "tmp", attention: true},
11531153 },
@@ -1219,7 +1219,7 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
12191219 theme.Palette[10] = theme.Palette[2]
12201220 theme.PaletteKnown = 1<<2 | 1<<10
12211221 resolved := themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})
1222- state := barState{status: statusSnapshot{session: "c"}, mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
1222+ state := barState{status: statusSnapshot{session: "c"}, mru: []recentRouteDisplay{{name: "a"}, {name: "b"}, {name: "c"}}}
12231223 row := make([]renderer.Cell, 16)
12241224
12251225 drawStatusBarState(row, state, resolved.Styles)
@@ -1235,7 +1235,7 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
12351235 }
12361236
12371237 func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
1238- state := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}}
1238+ state := barState{status: statusSnapshot{session: "cur"}, mru: []recentRouteDisplay{{name: "fresh"}, {name: "middle"}, {name: "old"}}}
12391239 row := make([]renderer.Cell, 21)
12401240
12411241 drawStatusBarState(row, state, resolveStyles(nil))
@@ -1247,9 +1247,9 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
12471247 }
12481248
12491249 func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
1250- mru := make([]recentSession, 0, maxMRUSessions)
1250+ mru := make([]recentRouteDisplay, 0, maxMRUSessions)
12511251 for i := 1; i <= maxMRUSessions; i++ {
1252- mru = append(mru, recentSession{name: "recent" + strconv.Itoa(i)})
1252+ mru = append(mru, recentRouteDisplay{name: "recent" + strconv.Itoa(i)})
12531253 }
12541254 state := barState{status: statusSnapshot{session: "cur"}, mru: mru}
12551255
@@ -1309,27 +1309,27 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
13091309 {
13101310 name: "script text and copy feedback",
13111311 width: 32,
1312- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentSession{{name: "a"}}},
1312+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "a"}}},
13131313 wantContains: []string{" a"},
13141314 wantSuffix: " main ↑3 * copied",
13151315 },
13161316 {
13171317 name: "hide on overlap",
13181318 width: 16,
1319- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}},
1319+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}},
13201320 want: " cur ",
13211321 wantNotContains: []string{"main", "copied"},
13221322 },
13231323 {
13241324 name: "empty script keeps copy feedback behavior",
13251325 width: 12,
1326- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "", statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}},
1326+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}},
13271327 want: " cur copied",
13281328 },
13291329 {
13301330 name: "mru whole entry fitting with script text",
13311331 width: 24,
1332- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}},
1332+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentRouteDisplay{{name: "fresh"}, {name: "middle"}, {name: "old"}}},
13331333 wantContains: []string{" fresh"},
13341334 wantNotContains: []string{"middle"},
13351335 wantSuffix: " git",
@@ -1337,7 +1337,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
13371337 {
13381338 name: "mru fitting reserves wide right anchor width",
13391339 width: 12,
1340- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "界界", mru: []recentSession{{name: "a"}}},
1340+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "界界", mru: []recentRouteDisplay{{name: "a"}}},
13411341 wantNotContains: []string{" a "},
13421342 wantSuffix: " 界 界 ",
13431343 continuationCells: []int{9, 11},
@@ -1345,7 +1345,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
13451345 {
13461346 name: "mru fitting counts wide session names",
13471347 width: 20,
1348- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentSession{{name: "界"}, {name: "界"}, {name: "b"}}},
1348+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentRouteDisplay{{name: "界"}, {name: "界"}, {name: "b"}}},
13491349 wantContains: []string{" 界 "},
13501350 wantNotContains: []string{" b "},
13511351 wantSuffix: " git",
@@ -1378,7 +1378,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
13781378 }
13791379
13801380 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
1381- state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}
1381+ state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentRouteDisplay{{name: "a"}, {name: "b"}, {name: "c"}}}
13821382 row := make([]renderer.Cell, 20)
13831383
13841384 drawStatusBarState(row, state, resolveStyles(nil))
@@ -1391,7 +1391,7 @@ func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
13911391 }
13921392
13931393 func TestStatusBarCopyFeedbackBoundaryWidths(t *testing.T) {
1394- state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}}
1394+ state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}}
13951395 tests := []struct {
13961396 name string
13971397 cols int
@@ -1448,7 +1448,7 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) {
14481448 state := barState{
14491449 status: statusSnapshot{session: "current"},
14501450 bottomRight: "script ok",
1451- mru: []recentSession{{name: "new"}, {name: "middle"}, {name: "old"}},
1451+ mru: []recentRouteDisplay{{name: "new"}, {name: "middle"}, {name: "old"}},
14521452 }
14531453 drawStatusBarState(row, state, styles)
14541454 for i := range len(" current ") {
@@ -1485,7 +1485,7 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) {
14851485 }
14861486
14871487 single := make([]renderer.Cell, 20)
1488- drawStatusBarState(single, barState{status: statusSnapshot{session: "current"}, mru: []recentSession{{name: "only"}}}, styles)
1488+ drawStatusBarState(single, barState{status: statusSnapshot{session: "current"}, mru: []recentRouteDisplay{{name: "only"}}}, styles)
14891489 only := strings.Index(rowText(single), "only")
14901490 require.GreaterOrEqual(t, only, 0)
14911491 require.True(t, single[only].Style.Equal(styles.SurfaceRecent), "a singleton MRU must retain the 22 percent recent surface")
@@ -1111,7 +1111,7 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
1111 drawStatusBarState(row, state.bars, resolveStyles(nil))1111 drawStatusBarState(row, state.bars, resolveStyles(nil))
1112 return rowText(row)1112 return rowText(row)
1113 }1113 }
1114- normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "vty"}, {name: "misc"}}}1114+ normal := barState{status: statusSnapshot{session: "cur"}, mru: []recentRouteDisplay{{name: "vty"}, {name: "misc"}}}
1115 ac.sendMu.Lock()1115 ac.sendMu.Lock()
1116 defer ac.sendMu.Unlock()1116 defer ac.sendMu.Unlock()
1117 1117
@@ -1147,7 +1147,7 @@ func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
1147 state := barState{1147 state := barState{
1148 status: statusSnapshot{session: "cur"},1148 status: statusSnapshot{session: "cur"},
1149 attentionFrame: 1,1149 attentionFrame: 1,
1150- mru: []recentSession{1150+ mru: []recentRouteDisplay{
1151 {name: "fresh"},1151 {name: "fresh"},
1152 {name: "tmp", attention: true},1152 {name: "tmp", attention: true},
1153 },1153 },
@@ -1219,7 +1219,7 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
1219 theme.Palette[10] = theme.Palette[2]1219 theme.Palette[10] = theme.Palette[2]
1220 theme.PaletteKnown = 1<<2 | 1<<101220 theme.PaletteKnown = 1<<2 | 1<<10
1221 resolved := themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})1221 resolved := themeui.Resolve(theme, domain.ThemeAccent{Mode: domain.ThemeAccentAuto})
1222- state := barState{status: statusSnapshot{session: "c"}, mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}1222+ state := barState{status: statusSnapshot{session: "c"}, mru: []recentRouteDisplay{{name: "a"}, {name: "b"}, {name: "c"}}}
1223 row := make([]renderer.Cell, 16)1223 row := make([]renderer.Cell, 16)
1224 1224
1225 drawStatusBarState(row, state, resolved.Styles)1225 drawStatusBarState(row, state, resolved.Styles)
@@ -1235,7 +1235,7 @@ func TestStatusBarMRUGradientTruecolorAndPlainFallback(t *testing.T) {
1235 }1235 }
1236 1236
1237 func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {1237 func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
1238- state := barState{status: statusSnapshot{session: "cur"}, mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}}1238+ state := barState{status: statusSnapshot{session: "cur"}, mru: []recentRouteDisplay{{name: "fresh"}, {name: "middle"}, {name: "old"}}}
1239 row := make([]renderer.Cell, 21)1239 row := make([]renderer.Cell, 21)
1240 1240
1241 drawStatusBarState(row, state, resolveStyles(nil))1241 drawStatusBarState(row, state, resolveStyles(nil))
@@ -1247,9 +1247,9 @@ func TestStatusBarNarrowRowsDropWholeOldestMRUEntries(t *testing.T) {
1247 }1247 }
1248 1248
1249 func TestStatusBarMRUWidthAwareBudget(t *testing.T) {1249 func TestStatusBarMRUWidthAwareBudget(t *testing.T) {
1250- mru := make([]recentSession, 0, maxMRUSessions)1250+ mru := make([]recentRouteDisplay, 0, maxMRUSessions)
1251 for i := 1; i <= maxMRUSessions; i++ {1251 for i := 1; i <= maxMRUSessions; i++ {
1252- mru = append(mru, recentSession{name: "recent" + strconv.Itoa(i)})1252+ mru = append(mru, recentRouteDisplay{name: "recent" + strconv.Itoa(i)})
1253 }1253 }
1254 state := barState{status: statusSnapshot{session: "cur"}, mru: mru}1254 state := barState{status: statusSnapshot{session: "cur"}, mru: mru}
1255 1255
@@ -1309,27 +1309,27 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
1309 {1309 {
1310 name: "script text and copy feedback",1310 name: "script text and copy feedback",
1311 width: 32,1311 width: 32,
1312- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentSession{{name: "a"}}},1312+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "a"}}},
1313 wantContains: []string{" a"},1313 wantContains: []string{" a"},
1314 wantSuffix: " main ↑3 * copied",1314 wantSuffix: " main ↑3 * copied",
1315 },1315 },
1316 {1316 {
1317 name: "hide on overlap",1317 name: "hide on overlap",
1318 width: 16,1318 width: 16,
1319- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}},1319+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "main ↑3 *", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}},
1320 want: " cur ",1320 want: " cur ",
1321 wantNotContains: []string{"main", "copied"},1321 wantNotContains: []string{"main", "copied"},
1322 },1322 },
1323 {1323 {
1324 name: "empty script keeps copy feedback behavior",1324 name: "empty script keeps copy feedback behavior",
1325 width: 12,1325 width: 12,
1326- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "", statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}},1326+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "", statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}},
1327 want: " cur copied",1327 want: " cur copied",
1328 },1328 },
1329 {1329 {
1330 name: "mru whole entry fitting with script text",1330 name: "mru whole entry fitting with script text",
1331 width: 24,1331 width: 24,
1332- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentSession{{name: "fresh"}, {name: "middle"}, {name: "old"}}},1332+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentRouteDisplay{{name: "fresh"}, {name: "middle"}, {name: "old"}}},
1333 wantContains: []string{" fresh"},1333 wantContains: []string{" fresh"},
1334 wantNotContains: []string{"middle"},1334 wantNotContains: []string{"middle"},
1335 wantSuffix: " git",1335 wantSuffix: " git",
@@ -1337,7 +1337,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
1337 {1337 {
1338 name: "mru fitting reserves wide right anchor width",1338 name: "mru fitting reserves wide right anchor width",
1339 width: 12,1339 width: 12,
1340- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "界界", mru: []recentSession{{name: "a"}}},1340+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "界界", mru: []recentRouteDisplay{{name: "a"}}},
1341 wantNotContains: []string{" a "},1341 wantNotContains: []string{" a "},
1342 wantSuffix: " 界 界 ",1342 wantSuffix: " 界 界 ",
1343 continuationCells: []int{9, 11},1343 continuationCells: []int{9, 11},
@@ -1345,7 +1345,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
1345 {1345 {
1346 name: "mru fitting counts wide session names",1346 name: "mru fitting counts wide session names",
1347 width: 20,1347 width: 20,
1348- state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentSession{{name: "界"}, {name: "界"}, {name: "b"}}},1348+ state: barState{status: statusSnapshot{session: "cur"}, bottomRight: "git", mru: []recentRouteDisplay{{name: "界"}, {name: "界"}, {name: "b"}}},
1349 wantContains: []string{" 界 "},1349 wantContains: []string{" 界 "},
1350 wantNotContains: []string{" b "},1350 wantNotContains: []string{" b "},
1351 wantSuffix: " git",1351 wantSuffix: " git",
@@ -1378,7 +1378,7 @@ func TestStatusBarBottomRightAndMRUFitting(t *testing.T) {
1378 }1378 }
1379 1379
1380 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {1380 func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
1381- state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentSession{{name: "a"}, {name: "b"}, {name: "c"}}}1381+ state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentRouteDisplay{{name: "a"}, {name: "b"}, {name: "c"}}}
1382 row := make([]renderer.Cell, 20)1382 row := make([]renderer.Cell, 20)
1383 1383
1384 drawStatusBarState(row, state, resolveStyles(nil))1384 drawStatusBarState(row, state, resolveStyles(nil))
@@ -1391,7 +1391,7 @@ func TestStatusBarCopyFeedbackFullyRenderedAlongsideMRU(t *testing.T) {
1391 }1391 }
1392 1392
1393 func TestStatusBarCopyFeedbackBoundaryWidths(t *testing.T) {1393 func TestStatusBarCopyFeedbackBoundaryWidths(t *testing.T) {
1394- state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentSession{{name: "fresh"}}}1394+ state := barState{status: statusSnapshot{session: "cur"}, statusFeedback: "copied", mru: []recentRouteDisplay{{name: "fresh"}}}
1395 tests := []struct {1395 tests := []struct {
1396 name string1396 name string
1397 cols int1397 cols int
@@ -1448,7 +1448,7 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) {
1448 state := barState{1448 state := barState{
1449 status: statusSnapshot{session: "current"},1449 status: statusSnapshot{session: "current"},
1450 bottomRight: "script ok",1450 bottomRight: "script ok",
1451- mru: []recentSession{{name: "new"}, {name: "middle"}, {name: "old"}},1451+ mru: []recentRouteDisplay{{name: "new"}, {name: "middle"}, {name: "old"}},
1452 }1452 }
1453 drawStatusBarState(row, state, styles)1453 drawStatusBarState(row, state, styles)
1454 for i := range len(" current ") {1454 for i := range len(" current ") {
@@ -1485,7 +1485,7 @@ func TestStatusBarsUseCompleteSemanticSurfaces(t *testing.T) {
1485 }1485 }
1486 1486
1487 single := make([]renderer.Cell, 20)1487 single := make([]renderer.Cell, 20)
1488- drawStatusBarState(single, barState{status: statusSnapshot{session: "current"}, mru: []recentSession{{name: "only"}}}, styles)1488+ drawStatusBarState(single, barState{status: statusSnapshot{session: "current"}, mru: []recentRouteDisplay{{name: "only"}}}, styles)
1489 only := strings.Index(rowText(single), "only")1489 only := strings.Index(rowText(single), "only")
1490 require.GreaterOrEqual(t, only, 0)1490 require.GreaterOrEqual(t, only, 0)
1491 require.True(t, single[only].Style.Equal(styles.SurfaceRecent), "a singleton MRU must retain the 22 percent recent surface")1491 require.True(t, single[only].Style.Equal(styles.SurfaceRecent), "a singleton MRU must retain the 22 percent recent surface")

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