detached 042317d

Merge pull request #99 from bnema/fix/status-mru-exclude-ephemeral

fix(status): exclude ephemeral sessions from recent history
commit
042317d52bf8dff8f12c22a740a6ee7d68bfac3e
author
bnema · · 2026-07-15 19:17 UTC
committer
GitHub · · 2026-07-15 19:17 UTC
merge of
71431fb + 541aedf · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
5 files · +77 -34
M internal/usecase/daemon/palette_test.go +19 -0
@@ -73,6 +73,25 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
7373 }, got)
7474 }
7575
76+func TestPaletteRecentSessionsExcludeEphemeralSessions(t *testing.T) {
77+ p, release := newBlockingPTY(t)
78+ defer release()
79+ d, current, ac, _ := newManualSessionWithPTYs(t, p)
80+ named := &session{id: "named", name: "named", tabs: []*tab{{}}}
81+ named.mruAt.Store(1)
82+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
83+ ephemeral.mruAt.Store(2)
84+ d.sessions[named.id] = named
85+ d.sessions[ephemeral.id] = ephemeral
86+
87+ d.enterPalette(current, ac)
88+ ac.overlays.paletteMu.Lock()
89+ defer ac.overlays.paletteMu.Unlock()
90+
91+ require.Len(t, ac.overlays.paletteRecent, 1)
92+ require.Equal(t, named.id, ac.overlays.paletteRecent[0].id)
93+}
94+
7695 func TestPaletteSessionFailureFeedbackClearsOnQueryChange(t *testing.T) {
7796 p, release := newBlockingPTY(t)
7897 defer release()
@@ -73,6 +73,25 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
73 }, got)73 }, got)
74 }74 }
75 75
76+func TestPaletteRecentSessionsExcludeEphemeralSessions(t *testing.T) {
77+ p, release := newBlockingPTY(t)
78+ defer release()
79+ d, current, ac, _ := newManualSessionWithPTYs(t, p)
80+ named := &session{id: "named", name: "named", tabs: []*tab{{}}}
81+ named.mruAt.Store(1)
82+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
83+ ephemeral.mruAt.Store(2)
84+ d.sessions[named.id] = named
85+ d.sessions[ephemeral.id] = ephemeral
86+
87+ d.enterPalette(current, ac)
88+ ac.overlays.paletteMu.Lock()
89+ defer ac.overlays.paletteMu.Unlock()
90+
91+ require.Len(t, ac.overlays.paletteRecent, 1)
92+ require.Equal(t, named.id, ac.overlays.paletteRecent[0].id)
93+}
94+
76 func TestPaletteSessionFailureFeedbackClearsOnQueryChange(t *testing.T) {95 func TestPaletteSessionFailureFeedbackClearsOnQueryChange(t *testing.T) {
77 p, release := newBlockingPTY(t)96 p, release := newBlockingPTY(t)
78 defer release()97 defer release()
M internal/usecase/daemon/picker_test.go +21 -0
@@ -77,6 +77,27 @@ func TestPickerViewsAddsBellSuffixForAttention(t *testing.T) {
7777 require.NotNil(t, target.ExpectedCreatedAt)
7878 }
7979
80+func TestPickerViewsIncludesEphemeralSessions(t *testing.T) {
81+ d := newTestDaemon(t, nil, stubClock{})
82+ current := &session{id: "current", name: "current", tabs: []*tab{{}}}
83+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
84+ d.sessions[current.id] = current
85+ d.sessions[ephemeral.id] = ephemeral
86+
87+ views, _ := d.pickerViews(current)
88+
89+ require.Len(t, views, 2)
90+ var ephemeralView picker.SessionView
91+ for _, view := range views {
92+ if view.ID == ephemeral.id {
93+ ephemeralView = view
94+ break
95+ }
96+ }
97+ require.Equal(t, ephemeral.id, ephemeralView.ID)
98+ require.Equal(t, "1", ephemeralView.Name)
99+}
100+
80101 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
81102 d := newTestDaemon(t, nil, stubClock{})
82103 ctx, cancel := context.WithCancel(d.serveCtx)
@@ -77,6 +77,27 @@ func TestPickerViewsAddsBellSuffixForAttention(t *testing.T) {
77 require.NotNil(t, target.ExpectedCreatedAt)77 require.NotNil(t, target.ExpectedCreatedAt)
78 }78 }
79 79
80+func TestPickerViewsIncludesEphemeralSessions(t *testing.T) {
81+ d := newTestDaemon(t, nil, stubClock{})
82+ current := &session{id: "current", name: "current", tabs: []*tab{{}}}
83+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
84+ d.sessions[current.id] = current
85+ d.sessions[ephemeral.id] = ephemeral
86+
87+ views, _ := d.pickerViews(current)
88+
89+ require.Len(t, views, 2)
90+ var ephemeralView picker.SessionView
91+ for _, view := range views {
92+ if view.ID == ephemeral.id {
93+ ephemeralView = view
94+ break
95+ }
96+ }
97+ require.Equal(t, ephemeral.id, ephemeralView.ID)
98+ require.Equal(t, "1", ephemeralView.Name)
99+}
100+
80 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {101 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
81 d := newTestDaemon(t, nil, stubClock{})102 d := newTestDaemon(t, nil, stubClock{})
82 ctx, cancel := context.WithCancel(d.serveCtx)103 ctx, cancel := context.WithCancel(d.serveCtx)
M internal/usecase/daemon/recent_sessions.go +9 -7
@@ -11,13 +11,12 @@ import (
1111 type recentSession struct {
1212 id domain.SessionID
1313 name string
14- ephemeral bool
1514 attention bool
1615 mruAt uint64
1716 }
1817
19-// recentSessions returns the current session's capped MRU list. Callers may
20-// retain the returned values for the lifetime of an interaction.
18+// recentSessions returns the current session's capped named-session MRU list.
19+// Callers may retain the returned values for the lifetime of an interaction.
2120 func (d *Daemon) recentSessions(current *session) []recentSession {
2221 if d == nil {
2322 return nil
@@ -29,11 +28,14 @@ func (d *Daemon) recentSessions(current *session) []recentSession {
2928 continue
3029 }
3130 sess.mu.Lock()
31+ if sess.ephemeral {
32+ sess.mu.Unlock()
33+ continue
34+ }
3235 entry := recentSession{
33- id: sess.id,
34- name: sess.name,
35- ephemeral: sess.ephemeral,
36- mruAt: sess.mruAt.Load(),
36+ id: sess.id,
37+ name: sess.name,
38+ mruAt: sess.mruAt.Load(),
3739 }
3840 for _, tb := range sess.tabs {
3941 if tb.attention {
@@ -11,13 +11,12 @@ import (
11 type recentSession struct {11 type recentSession struct {
12 id domain.SessionID12 id domain.SessionID
13 name string13 name string
14- ephemeral bool
15 attention bool14 attention bool
16 mruAt uint6415 mruAt uint64
17 }16 }
18 17
19-// recentSessions returns the current session's capped MRU list. Callers may18+// recentSessions returns the current session's capped named-session MRU list.
20-// retain the returned values for the lifetime of an interaction.19+// Callers may retain the returned values for the lifetime of an interaction.
21 func (d *Daemon) recentSessions(current *session) []recentSession {20 func (d *Daemon) recentSessions(current *session) []recentSession {
22 if d == nil {21 if d == nil {
23 return nil22 return nil
@@ -29,11 +28,14 @@ func (d *Daemon) recentSessions(current *session) []recentSession {
29 continue28 continue
30 }29 }
31 sess.mu.Lock()30 sess.mu.Lock()
31+ if sess.ephemeral {
32+ sess.mu.Unlock()
33+ continue
34+ }
32 entry := recentSession{35 entry := recentSession{
33- id: sess.id,36+ id: sess.id,
34- name: sess.name,37+ name: sess.name,
35- ephemeral: sess.ephemeral,38+ mruAt: sess.mruAt.Load(),
36- mruAt: sess.mruAt.Load(),
37 }39 }
38 for _, tb := range sess.tabs {40 for _, tb := range sess.tabs {
39 if tb.attention {41 if tb.attention {
M internal/usecase/daemon/status.go +5 -22
@@ -93,7 +93,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
9393 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
9494 for i, sess := range fittedMRU {
9595 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))
96- drawStatusSessionEntry(row, &x, sess.name, sess.ephemeral, sess.attention, style, state.attentionFrame)
96+ drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)
9797 }
9898 }
9999 drawRightPlainText(row, rightText, x, styles.statusBar)
@@ -102,7 +102,6 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
102102 type rankedRecent struct {
103103 rank int
104104 name string
105- ephemeral bool
106105 attention bool
107106 selected bool
108107 }
@@ -111,11 +110,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
111110 prefixStyle := style
112111 prefixStyle.Bold = true
113112 writeStatusText(row, x, " "+strconv.Itoa(sess.rank)+":", prefixStyle)
114- name := sess.name
115- if sess.ephemeral {
116- name += "*"
117- }
118- writeStatusText(row, x, name, style)
113+ writeStatusText(row, x, sess.name, style)
119114 if sess.attention {
120115 writeStatusText(row, x, " ", style)
121116 writeBell(row, x, attentionFrame, style)
@@ -123,10 +118,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
123118 writeStatusText(row, x, " ", style)
124119 }
125120
126-func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, ephemeral, attention bool, style renderer.Style, attentionFrame int) {
127- if ephemeral {
128- name += "*"
129- }
121+func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, attention bool, style renderer.Style, attentionFrame int) {
130122 writeStatusText(row, x, " "+name, style)
131123 if attention {
132124 writeStatusText(row, x, " ", style)
@@ -228,7 +220,6 @@ func rankedRecentForHints(hints *palette.ContextualHints, recent []recentSession
228220 // recent was copied with the hint under paletteMu, so this only enriches
229221 // the render snapshot and never performs a live domain lookup.
230222 if i < len(recent) {
231- entry.ephemeral = recent[i].ephemeral
232223 entry.attention = recent[i].attention
233224 }
234225 entries = append(entries, entry)
@@ -364,11 +355,7 @@ func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback stri
364355 return nil
365356 }
366357 cost := func(e rankedRecent) int {
367- name := e.name
368- if e.ephemeral {
369- name += "*"
370- }
371- width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(name)
358+ width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(e.name)
372359 if e.attention {
373360 width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
374361 }
@@ -396,11 +383,7 @@ func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []re
396383 return nil
397384 }
398385 cost := func(e recentSession) int {
399- name := e.name
400- if e.ephemeral {
401- name += "*"
402- }
403- n := 2 + statusTextWidth(name)
386+ n := 2 + statusTextWidth(e.name)
404387 if e.attention {
405388 n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
406389 }
@@ -93,7 +93,7 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
93 fittedMRU := fitMRU(state.mru, len(row), x, rightText)93 fittedMRU := fitMRU(state.mru, len(row), x, rightText)
94 for i, sess := range fittedMRU {94 for i, sess := range fittedMRU {
95 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))95 style := mruStyle(styles.statusBar, state.theme, i, len(fittedMRU))
96- drawStatusSessionEntry(row, &x, sess.name, sess.ephemeral, sess.attention, style, state.attentionFrame)96+ drawStatusSessionEntry(row, &x, sess.name, sess.attention, style, state.attentionFrame)
97 }97 }
98 }98 }
99 drawRightPlainText(row, rightText, x, styles.statusBar)99 drawRightPlainText(row, rightText, x, styles.statusBar)
@@ -102,7 +102,6 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles)
102 type rankedRecent struct {102 type rankedRecent struct {
103 rank int103 rank int
104 name string104 name string
105- ephemeral bool
106 attention bool105 attention bool
107 selected bool106 selected bool
108 }107 }
@@ -111,11 +110,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
111 prefixStyle := style110 prefixStyle := style
112 prefixStyle.Bold = true111 prefixStyle.Bold = true
113 writeStatusText(row, x, " "+strconv.Itoa(sess.rank)+":", prefixStyle)112 writeStatusText(row, x, " "+strconv.Itoa(sess.rank)+":", prefixStyle)
114- name := sess.name113+ writeStatusText(row, x, sess.name, style)
115- if sess.ephemeral {
116- name += "*"
117- }
118- writeStatusText(row, x, name, style)
119 if sess.attention {114 if sess.attention {
120 writeStatusText(row, x, " ", style)115 writeStatusText(row, x, " ", style)
121 writeBell(row, x, attentionFrame, style)116 writeBell(row, x, attentionFrame, style)
@@ -123,10 +118,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent
123 writeStatusText(row, x, " ", style)118 writeStatusText(row, x, " ", style)
124 }119 }
125 120
126-func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, ephemeral, attention bool, style renderer.Style, attentionFrame int) {121+func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, attention bool, style renderer.Style, attentionFrame int) {
127- if ephemeral {
128- name += "*"
129- }
130 writeStatusText(row, x, " "+name, style)122 writeStatusText(row, x, " "+name, style)
131 if attention {123 if attention {
132 writeStatusText(row, x, " ", style)124 writeStatusText(row, x, " ", style)
@@ -228,7 +220,6 @@ func rankedRecentForHints(hints *palette.ContextualHints, recent []recentSession
228 // recent was copied with the hint under paletteMu, so this only enriches220 // recent was copied with the hint under paletteMu, so this only enriches
229 // the render snapshot and never performs a live domain lookup.221 // the render snapshot and never performs a live domain lookup.
230 if i < len(recent) {222 if i < len(recent) {
231- entry.ephemeral = recent[i].ephemeral
232 entry.attention = recent[i].attention223 entry.attention = recent[i].attention
233 }224 }
234 entries = append(entries, entry)225 entries = append(entries, entry)
@@ -364,11 +355,7 @@ func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback stri
364 return nil355 return nil
365 }356 }
366 cost := func(e rankedRecent) int {357 cost := func(e rankedRecent) int {
367- name := e.name358+ width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(e.name)
368- if e.ephemeral {
369- name += "*"
370- }
371- width := 2 + statusTextWidth(strconv.Itoa(e.rank)+":") + statusTextWidth(name)
372 if e.attention {359 if e.attention {
373 width += 1 + renderer.RuneWidth(ui.AttentionGlyph)360 width += 1 + renderer.RuneWidth(ui.AttentionGlyph)
374 }361 }
@@ -396,11 +383,7 @@ func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []re
396 return nil383 return nil
397 }384 }
398 cost := func(e recentSession) int {385 cost := func(e recentSession) int {
399- name := e.name386+ n := 2 + statusTextWidth(e.name)
400- if e.ephemeral {
401- name += "*"
402- }
403- n := 2 + statusTextWidth(name)
404 if e.attention {387 if e.attention {
405 n += 1 + renderer.RuneWidth(ui.AttentionGlyph)388 n += 1 + renderer.RuneWidth(ui.AttentionGlyph)
406 }389 }
M internal/usecase/daemon/status_test.go +23 -5
@@ -978,7 +978,7 @@ func TestBarStateForContextualRecentUsesSnapshotAndNormalUsesLiveMRU(t *testing.
978978 done := make(chan struct{})
979979 d.mu.Lock()
980980 go func() {
981- contextual = d.barStateForPaletteHints(sess, "", &hints, []recentSession{{name: "captured", ephemeral: true, attention: true}})
981+ contextual = d.barStateForPaletteHints(sess, "", &hints, []recentSession{{name: "captured", attention: true}})
982982 close(done)
983983 }()
984984 select {
@@ -990,7 +990,7 @@ func TestBarStateForContextualRecentUsesSnapshotAndNormalUsesLiveMRU(t *testing.
990990 d.mu.Unlock()
991991
992992 require.Empty(t, contextual.mru, "contextual composition must not read the live MRU")
993- require.Equal(t, []rankedRecent{{rank: 1, name: "captured", ephemeral: true, attention: true, selected: true}}, contextual.rankedRecent)
993+ require.Equal(t, []rankedRecent{{rank: 1, name: "captured", attention: true, selected: true}}, contextual.rankedRecent)
994994
995995 normal := d.barStateFor(sess, "")
996996 require.Len(t, normal.mru, 1)
@@ -1020,6 +1020,24 @@ func TestBarStateForMRUFreshestFirstCapCurrentExcludedAndAttention(t *testing.T)
10201020 require.True(t, state.mru[1].attention, "s8 attention should be carried into MRU state")
10211021 }
10221022
1023+func TestBarStateForMRUExcludesEphemeralSessions(t *testing.T) {
1024+ p, releasePTY := newBlockingPTY(t)
1025+ d, current, _, _ := newManualSessionWithPTYs(t, p)
1026+ defer releasePTY()
1027+
1028+ persistent := &session{id: "named", name: "named", tabs: []*tab{{}}}
1029+ persistent.mruAt.Store(1)
1030+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
1031+ ephemeral.mruAt.Store(2)
1032+ d.sessions[persistent.id] = persistent
1033+ d.sessions[ephemeral.id] = ephemeral
1034+
1035+ state := d.barStateFor(current, "")
1036+
1037+ require.Len(t, state.mru, 1)
1038+ require.Equal(t, "named", state.mru[0].name)
1039+}
1040+
10231041 func TestBarStateForMRUZeroTimesUseDeterministicNameOrder(t *testing.T) {
10241042 p, releasePTY := newBlockingPTY(t)
10251043 d, sess, _, _ := newManualSessionWithPTYs(t, p)
@@ -1110,20 +1128,20 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
11101128 require.Contains(t, draw(normalAfterPopulated), "vty")
11111129 }
11121130
1113-func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) {
1131+func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
11141132 state := barState{
11151133 status: statusSnapshot{session: "cur"},
11161134 attentionFrame: 1,
11171135 mru: []recentSession{
11181136 {name: "fresh"},
1119- {name: "tmp", ephemeral: true, attention: true},
1137+ {name: "tmp", attention: true},
11201138 },
11211139 }
11221140 row := make([]renderer.Cell, 24)
11231141
11241142 drawStatusBarState(row, state, resolveThemeStyles(nil))
11251143
1126- require.Equal(t, " cur fresh tmp*  ", rowText(row))
1144+ require.Equal(t, " cur fresh tmp  ", rowText(row))
11271145 for _, c := range row {
11281146 if c.Rune == ui.AttentionGlyph {
11291147 require.True(t, c.Style.Bold)
@@ -978,7 +978,7 @@ func TestBarStateForContextualRecentUsesSnapshotAndNormalUsesLiveMRU(t *testing.
978 done := make(chan struct{})978 done := make(chan struct{})
979 d.mu.Lock()979 d.mu.Lock()
980 go func() {980 go func() {
981- contextual = d.barStateForPaletteHints(sess, "", &hints, []recentSession{{name: "captured", ephemeral: true, attention: true}})981+ contextual = d.barStateForPaletteHints(sess, "", &hints, []recentSession{{name: "captured", attention: true}})
982 close(done)982 close(done)
983 }()983 }()
984 select {984 select {
@@ -990,7 +990,7 @@ func TestBarStateForContextualRecentUsesSnapshotAndNormalUsesLiveMRU(t *testing.
990 d.mu.Unlock()990 d.mu.Unlock()
991 991
992 require.Empty(t, contextual.mru, "contextual composition must not read the live MRU")992 require.Empty(t, contextual.mru, "contextual composition must not read the live MRU")
993- require.Equal(t, []rankedRecent{{rank: 1, name: "captured", ephemeral: true, attention: true, selected: true}}, contextual.rankedRecent)993+ require.Equal(t, []rankedRecent{{rank: 1, name: "captured", attention: true, selected: true}}, contextual.rankedRecent)
994 994
995 normal := d.barStateFor(sess, "")995 normal := d.barStateFor(sess, "")
996 require.Len(t, normal.mru, 1)996 require.Len(t, normal.mru, 1)
@@ -1020,6 +1020,24 @@ func TestBarStateForMRUFreshestFirstCapCurrentExcludedAndAttention(t *testing.T)
1020 require.True(t, state.mru[1].attention, "s8 attention should be carried into MRU state")1020 require.True(t, state.mru[1].attention, "s8 attention should be carried into MRU state")
1021 }1021 }
1022 1022
1023+func TestBarStateForMRUExcludesEphemeralSessions(t *testing.T) {
1024+ p, releasePTY := newBlockingPTY(t)
1025+ d, current, _, _ := newManualSessionWithPTYs(t, p)
1026+ defer releasePTY()
1027+
1028+ persistent := &session{id: "named", name: "named", tabs: []*tab{{}}}
1029+ persistent.mruAt.Store(1)
1030+ ephemeral := &session{id: "ephemeral", name: "1", ephemeral: true, tabs: []*tab{{}}}
1031+ ephemeral.mruAt.Store(2)
1032+ d.sessions[persistent.id] = persistent
1033+ d.sessions[ephemeral.id] = ephemeral
1034+
1035+ state := d.barStateFor(current, "")
1036+
1037+ require.Len(t, state.mru, 1)
1038+ require.Equal(t, "named", state.mru[0].name)
1039+}
1040+
1023 func TestBarStateForMRUZeroTimesUseDeterministicNameOrder(t *testing.T) {1041 func TestBarStateForMRUZeroTimesUseDeterministicNameOrder(t *testing.T) {
1024 p, releasePTY := newBlockingPTY(t)1042 p, releasePTY := newBlockingPTY(t)
1025 d, sess, _, _ := newManualSessionWithPTYs(t, p)1043 d, sess, _, _ := newManualSessionWithPTYs(t, p)
@@ -1110,20 +1128,20 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse(
1110 require.Contains(t, draw(normalAfterPopulated), "vty")1128 require.Contains(t, draw(normalAfterPopulated), "vty")
1111 }1129 }
1112 1130
1113-func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) {1131+func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) {
1114 state := barState{1132 state := barState{
1115 status: statusSnapshot{session: "cur"},1133 status: statusSnapshot{session: "cur"},
1116 attentionFrame: 1,1134 attentionFrame: 1,
1117 mru: []recentSession{1135 mru: []recentSession{
1118 {name: "fresh"},1136 {name: "fresh"},
1119- {name: "tmp", ephemeral: true, attention: true},1137+ {name: "tmp", attention: true},
1120 },1138 },
1121 }1139 }
1122 row := make([]renderer.Cell, 24)1140 row := make([]renderer.Cell, 24)
1123 1141
1124 drawStatusBarState(row, state, resolveThemeStyles(nil))1142 drawStatusBarState(row, state, resolveThemeStyles(nil))
1125 1143
1126- require.Equal(t, " cur fresh tmp*  ", rowText(row))1144+ require.Equal(t, " cur fresh tmp  ", rowText(row))
1127 for _, c := range row {1145 for _, c := range row {
1128 if c.Rune == ui.AttentionGlyph {1146 if c.Rune == ui.AttentionGlyph {
1129 require.True(t, c.Style.Bold)1147 require.True(t, c.Style.Bold)

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