Merge pull request #99 from bnema/fix/status-mru-exclude-ephemeral
fix(status): exclude ephemeral sessions from recent history
5 files · +77 -34
M internal/usecase/daemon/palette_test.go +19 -0
| @@ -73,6 +73,25 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) { | |||
| 73 | 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 | 95 | func TestPaletteSessionFailureFeedbackClearsOnQueryChange(t *testing.T) { | |
| 77 | 96 | p, release := newBlockingPTY(t) | |
| 78 | 97 | 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) { | |||
| 77 | 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 | 101 | func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) { | |
| 81 | 102 | d := newTestDaemon(t, nil, stubClock{}) | |
| 82 | 103 | 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 ( | |||
| 11 | 11 | type recentSession struct { | |
| 12 | 12 | id domain.SessionID | |
| 13 | 13 | name string | |
| 14 | - | ephemeral bool | |
| 15 | 14 | attention bool | |
| 16 | 15 | mruAt uint64 | |
| 17 | 16 | } | |
| 18 | 17 | ||
| 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. | |
| 21 | 20 | func (d *Daemon) recentSessions(current *session) []recentSession { | |
| 22 | 21 | if d == nil { | |
| 23 | 22 | return nil | |
| @@ -29,11 +28,14 @@ func (d *Daemon) recentSessions(current *session) []recentSession { | |||
| 29 | 28 | continue | |
| 30 | 29 | } | |
| 31 | 30 | sess.mu.Lock() | |
| 31 | + | if sess.ephemeral { | |
| 32 | + | sess.mu.Unlock() | |
| 33 | + | continue | |
| 34 | + | } | |
| 32 | 35 | 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(), | |
| 37 | 39 | } | |
| 38 | 40 | for _, tb := range sess.tabs { | |
| 39 | 41 | if tb.attention { | |
| @@ -11,13 +11,12 @@ import ( | |||||
| 11 | type recentSession struct { | 11 | type recentSession struct { | ||
| 12 | id domain.SessionID | 12 | id domain.SessionID | ||
| 13 | name string | 13 | name string | ||
| 14 | - | ephemeral bool | |||
| 15 | attention bool | 14 | attention bool | ||
| 16 | mruAt uint64 | 15 | mruAt uint64 | ||
| 17 | } | 16 | } | ||
| 18 | 17 | ||||
| 19 | - | // recentSessions returns the current session's capped MRU list. Callers may | 18 | + | // 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 nil | 22 | return nil | ||
| @@ -29,11 +28,14 @@ func (d *Daemon) recentSessions(current *session) []recentSession { | |||||
| 29 | continue | 28 | 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) | |||
| 93 | 93 | fittedMRU := fitMRU(state.mru, len(row), x, rightText) | |
| 94 | 94 | for i, sess := range fittedMRU { | |
| 95 | 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 | 99 | drawRightPlainText(row, rightText, x, styles.statusBar) | |
| @@ -102,7 +102,6 @@ func drawStatusBarState(row []renderer.Cell, state barState, styles themeStyles) | |||
| 102 | 102 | type rankedRecent struct { | |
| 103 | 103 | rank int | |
| 104 | 104 | name string | |
| 105 | - | ephemeral bool | |
| 106 | 105 | attention bool | |
| 107 | 106 | selected bool | |
| 108 | 107 | } | |
| @@ -111,11 +110,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent | |||
| 111 | 110 | prefixStyle := style | |
| 112 | 111 | prefixStyle.Bold = true | |
| 113 | 112 | 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) | |
| 119 | 114 | if sess.attention { | |
| 120 | 115 | writeStatusText(row, x, " ", style) | |
| 121 | 116 | writeBell(row, x, attentionFrame, style) | |
| @@ -123,10 +118,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent | |||
| 123 | 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) { | |
| 127 | - | if ephemeral { | |
| 128 | - | name += "*" | |
| 129 | - | } | |
| 121 | + | func drawStatusSessionEntry(row []renderer.Cell, x *int, name string, attention bool, style renderer.Style, attentionFrame int) { | |
| 130 | 122 | writeStatusText(row, x, " "+name, style) | |
| 131 | 123 | if attention { | |
| 132 | 124 | writeStatusText(row, x, " ", style) | |
| @@ -228,7 +220,6 @@ func rankedRecentForHints(hints *palette.ContextualHints, recent []recentSession | |||
| 228 | 220 | // recent was copied with the hint under paletteMu, so this only enriches | |
| 229 | 221 | // the render snapshot and never performs a live domain lookup. | |
| 230 | 222 | if i < len(recent) { | |
| 231 | - | entry.ephemeral = recent[i].ephemeral | |
| 232 | 223 | entry.attention = recent[i].attention | |
| 233 | 224 | } | |
| 234 | 225 | entries = append(entries, entry) | |
| @@ -364,11 +355,7 @@ func fitRankedRecent(entries []rankedRecent, rowLen, leftUsed int, feedback stri | |||
| 364 | 355 | return nil | |
| 365 | 356 | } | |
| 366 | 357 | 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) | |
| 372 | 359 | if e.attention { | |
| 373 | 360 | width += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 374 | 361 | } | |
| @@ -396,11 +383,7 @@ func fitMRU(entries []recentSession, rowLen, leftUsed int, feedback string) []re | |||
| 396 | 383 | return nil | |
| 397 | 384 | } | |
| 398 | 385 | 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) | |
| 404 | 387 | if e.attention { | |
| 405 | 388 | n += 1 + renderer.RuneWidth(ui.AttentionGlyph) | |
| 406 | 389 | } | |
| @@ -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 int | 103 | rank int | ||
| 104 | name string | 104 | name string | ||
| 105 | - | ephemeral bool | |||
| 106 | attention bool | 105 | attention bool | ||
| 107 | selected bool | 106 | selected bool | ||
| 108 | } | 107 | } | ||
| @@ -111,11 +110,7 @@ func drawRankedStatusSessionEntry(row []renderer.Cell, x *int, sess rankedRecent | |||||
| 111 | prefixStyle := style | 110 | prefixStyle := style | ||
| 112 | prefixStyle.Bold = true | 111 | prefixStyle.Bold = true | ||
| 113 | writeStatusText(row, x, " "+strconv.Itoa(sess.rank)+":", prefixStyle) | 112 | writeStatusText(row, x, " "+strconv.Itoa(sess.rank)+":", prefixStyle) | ||
| 114 | - | name := sess.name | 113 | + | 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 enriches | 220 | // 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].attention | 223 | 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 nil | 355 | return nil | ||
| 365 | } | 356 | } | ||
| 366 | cost := func(e rankedRecent) int { | 357 | cost := func(e rankedRecent) int { | ||
| 367 | - | name := e.name | 358 | + | 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 nil | 383 | return nil | ||
| 397 | } | 384 | } | ||
| 398 | cost := func(e recentSession) int { | 385 | cost := func(e recentSession) int { | ||
| 399 | - | name := e.name | 386 | + | 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. | |||
| 978 | 978 | done := make(chan struct{}) | |
| 979 | 979 | d.mu.Lock() | |
| 980 | 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 | 982 | close(done) | |
| 983 | 983 | }() | |
| 984 | 984 | select { | |
| @@ -990,7 +990,7 @@ func TestBarStateForContextualRecentUsesSnapshotAndNormalUsesLiveMRU(t *testing. | |||
| 990 | 990 | d.mu.Unlock() | |
| 991 | 991 | ||
| 992 | 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 | 995 | normal := d.barStateFor(sess, "") | |
| 996 | 996 | require.Len(t, normal.mru, 1) | |
| @@ -1020,6 +1020,24 @@ func TestBarStateForMRUFreshestFirstCapCurrentExcludedAndAttention(t *testing.T) | |||
| 1020 | 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 | 1041 | func TestBarStateForMRUZeroTimesUseDeterministicNameOrder(t *testing.T) { | |
| 1024 | 1042 | p, releasePTY := newBlockingPTY(t) | |
| 1025 | 1043 | d, sess, _, _ := newManualSessionWithPTYs(t, p) | |
| @@ -1110,20 +1128,20 @@ func TestCapturePrimaryRenderStatePreservesContextualMRUModeThroughScratchReuse( | |||
| 1110 | 1128 | require.Contains(t, draw(normalAfterPopulated), "vty") | |
| 1111 | 1129 | } | |
| 1112 | 1130 | ||
| 1113 | - | func TestStatusBarRendersMRUNamesEphemeralAndInlineBell(t *testing.T) { | |
| 1131 | + | func TestStatusBarRendersMRUNamesAndInlineBell(t *testing.T) { | |
| 1114 | 1132 | state := barState{ | |
| 1115 | 1133 | status: statusSnapshot{session: "cur"}, | |
| 1116 | 1134 | attentionFrame: 1, | |
| 1117 | 1135 | mru: []recentSession{ | |
| 1118 | 1136 | {name: "fresh"}, | |
| 1119 | - | {name: "tmp", ephemeral: true, attention: true}, | |
| 1137 | + | {name: "tmp", attention: true}, | |
| 1120 | 1138 | }, | |
| 1121 | 1139 | } | |
| 1122 | 1140 | row := make([]renderer.Cell, 24) | |
| 1123 | 1141 | ||
| 1124 | 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 | 1145 | for _, c := range row { | |
| 1128 | 1146 | if c.Rune == ui.AttentionGlyph { | |
| 1129 | 1147 | 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) | ||