detached 7953828

fix(status): exclude ephemeral sessions from recent history

commit
795382872361375a63f14425a165290df0d5c6e4
author
brice · · 2026-07-15 16:41 UTC
parent
71431fb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +38 -2
M internal/usecase/daemon/picker_test.go +14 -0
@@ -77,6 +77,20 @@ 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+ require.Equal(t, ephemeral.id, views[0].ID)
91+ require.Equal(t, "1", views[0].Name)
92+}
93+
8094 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
8195 d := newTestDaemon(t, nil, stubClock{})
8296 ctx, cancel := context.WithCancel(d.serveCtx)
@@ -77,6 +77,20 @@ 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+ require.Equal(t, ephemeral.id, views[0].ID)
91+ require.Equal(t, "1", views[0].Name)
92+}
93+
80 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {94 func TestPickerViewsCarryNamedLifecycleIdentity(t *testing.T) {
81 d := newTestDaemon(t, nil, stubClock{})95 d := newTestDaemon(t, nil, stubClock{})
82 ctx, cancel := context.WithCancel(d.serveCtx)96 ctx, cancel := context.WithCancel(d.serveCtx)
M internal/usecase/daemon/recent_sessions.go +6 -2
@@ -16,8 +16,8 @@ type recentSession struct {
1616 mruAt uint64
1717 }
1818
19-// recentSessions returns the current session's capped MRU list. Callers may
20-// retain the returned values for the lifetime of an interaction.
19+// recentSessions returns the current session's capped named-session MRU list.
20+// Callers may retain the returned values for the lifetime of an interaction.
2121 func (d *Daemon) recentSessions(current *session) []recentSession {
2222 if d == nil {
2323 return nil
@@ -29,6 +29,10 @@ func (d *Daemon) recentSessions(current *session) []recentSession {
2929 continue
3030 }
3131 sess.mu.Lock()
32+ if sess.ephemeral {
33+ sess.mu.Unlock()
34+ continue
35+ }
3236 entry := recentSession{
3337 id: sess.id,
3438 name: sess.name,
@@ -16,8 +16,8 @@ type recentSession struct {
16 mruAt uint6416 mruAt uint64
17 }17 }
18 18
19-// recentSessions returns the current session's capped MRU list. Callers may19+// recentSessions returns the current session's capped named-session MRU list.
20-// retain the returned values for the lifetime of an interaction.20+// Callers may retain the returned values for the lifetime of an interaction.
21 func (d *Daemon) recentSessions(current *session) []recentSession {21 func (d *Daemon) recentSessions(current *session) []recentSession {
22 if d == nil {22 if d == nil {
23 return nil23 return nil
@@ -29,6 +29,10 @@ func (d *Daemon) recentSessions(current *session) []recentSession {
29 continue29 continue
30 }30 }
31 sess.mu.Lock()31 sess.mu.Lock()
32+ if sess.ephemeral {
33+ sess.mu.Unlock()
34+ continue
35+ }
32 entry := recentSession{36 entry := recentSession{
33 id: sess.id,37 id: sess.id,
34 name: sess.name,38 name: sess.name,
M internal/usecase/daemon/status_test.go +18 -0
@@ -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)
@@ -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)

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