detached 1521b5b

fix(picker): snapshot session fields under the session lock before sorting

commit
1521b5b10f7facc5fa9b2cb78a65fdd84c86c1c1
author
brice · · 2026-07-29 15:35 UTC
parent
d2bbb2e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +19 -8
M internal/usecase/daemon/picker.go +19 -8
@@ -87,17 +87,28 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF
8787 }
8888 }
8989 d.mu.Unlock()
90- // Snapshot mruAt once: comparators must not observe concurrent touchMRU
91- // updates mid-sort.
92- mru := make(map[*session]uint64, len(sessions))
90+ // Snapshot mruAt, name, and ephemeral once: comparators must not observe
91+ // concurrent touchMRU updates or a renameSession mutation (which mutates
92+ // name/ephemeral under sess.mu) mid-sort.
93+ type pickerSortSnapshot struct {
94+ mruAt uint64
95+ name string
96+ ephemeral bool
97+ }
98+ snap := make(map[*session]pickerSortSnapshot, len(sessions))
9399 for _, s := range sessions {
94- mru[s] = s.mruAt.Load()
100+ mruAt := s.mruAt.Load()
101+ s.mu.Lock()
102+ name := s.name
103+ ephemeral := s.ephemeral
104+ s.mu.Unlock()
105+ snap[s] = pickerSortSnapshot{mruAt: mruAt, name: name, ephemeral: ephemeral}
95106 }
96107 sort.Slice(sessions, func(i, j int) bool {
97- if mru[sessions[i]] != mru[sessions[j]] {
98- return mru[sessions[i]] > mru[sessions[j]]
108+ if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt {
109+ return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt
99110 }
100- return sessions[i].name < sessions[j].name
111+ return snap[sessions[i]].name < snap[sessions[j]].name
101112 })
102113 sort.Slice(stopped, func(i, j int) bool {
103114 if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq {
@@ -109,7 +120,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF
109120 // Stable partition: named sessions first, ephemeral after, each keeping
110121 // its MRU order from the sort above.
111122 sort.SliceStable(sessions, func(i, j int) bool {
112- return !sessions[i].ephemeral && sessions[j].ephemeral
123+ return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral
113124 })
114125 }
115126
@@ -87,17 +87,28 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF
87 }87 }
88 }88 }
89 d.mu.Unlock()89 d.mu.Unlock()
90- // Snapshot mruAt once: comparators must not observe concurrent touchMRU90+ // Snapshot mruAt, name, and ephemeral once: comparators must not observe
91- // updates mid-sort.91+ // concurrent touchMRU updates or a renameSession mutation (which mutates
92- mru := make(map[*session]uint64, len(sessions))92+ // name/ephemeral under sess.mu) mid-sort.
93+ type pickerSortSnapshot struct {
94+ mruAt uint64
95+ name string
96+ ephemeral bool
97+ }
98+ snap := make(map[*session]pickerSortSnapshot, len(sessions))
93 for _, s := range sessions {99 for _, s := range sessions {
94- mru[s] = s.mruAt.Load()100+ mruAt := s.mruAt.Load()
101+ s.mu.Lock()
102+ name := s.name
103+ ephemeral := s.ephemeral
104+ s.mu.Unlock()
105+ snap[s] = pickerSortSnapshot{mruAt: mruAt, name: name, ephemeral: ephemeral}
95 }106 }
96 sort.Slice(sessions, func(i, j int) bool {107 sort.Slice(sessions, func(i, j int) bool {
97- if mru[sessions[i]] != mru[sessions[j]] {108+ if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt {
98- return mru[sessions[i]] > mru[sessions[j]]109+ return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt
99 }110 }
100- return sessions[i].name < sessions[j].name111+ return snap[sessions[i]].name < snap[sessions[j]].name
101 })112 })
102 sort.Slice(stopped, func(i, j int) bool {113 sort.Slice(stopped, func(i, j int) bool {
103 if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq {114 if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq {
@@ -109,7 +120,7 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF
109 // Stable partition: named sessions first, ephemeral after, each keeping120 // Stable partition: named sessions first, ephemeral after, each keeping
110 // its MRU order from the sort above.121 // its MRU order from the sort above.
111 sort.SliceStable(sessions, func(i, j int) bool {122 sort.SliceStable(sessions, func(i, j int) bool {
112- return !sessions[i].ephemeral && sessions[j].ephemeral123+ return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral
113 })124 })
114 }125 }
115 126

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