refactor(daemon): pickerViews reads sessions through snapshotView
- commit
- 5090cbbc0b8d1975947d2fb07c4b27c81fb08717
- author
- brice · · 2026-07-29 19:36 UTC
- parent
- 6710d10
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +65 -63
M internal/usecase/daemon/picker.go +28 -62
| @@ -87,28 +87,30 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||
| 87 | 87 | } | |
| 88 | 88 | } | |
| 89 | 89 | d.mu.Unlock() | |
| 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)) | |
| 90 | + | ||
| 91 | + | for _, s := range sessions { | |
| 92 | + | d.refreshSessionFocusedTitles(s) | |
| 93 | + | } | |
| 94 | + | opts := viewOptions{tabDetails: true, focusedTitles: true, terminalTitle: d.currentTabsConfig().TerminalTitle} | |
| 95 | + | ||
| 96 | + | // One snapshot per session: sorting and view building read the same | |
| 97 | + | // capture, so comparators cannot observe a concurrent touchMRU or | |
| 98 | + | // renameSession mid-sort (the invariant previously enforced by the | |
| 99 | + | // pickerSortSnapshot map). | |
| 100 | + | snaps := make([]sessionView, 0, len(sessions)) | |
| 101 | + | var current picker.SourceFilter | |
| 99 | 102 | for _, s := range sessions { | |
| 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} | |
| 106 | - | } | |
| 107 | - | sort.Slice(sessions, func(i, j int) bool { | |
| 108 | - | if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt { | |
| 109 | - | return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt | |
| 110 | - | } | |
| 111 | - | return snap[sessions[i]].name < snap[sessions[j]].name | |
| 103 | + | snap := s.snapshotView(opts) | |
| 104 | + | if s == cur && snap.active >= 0 && snap.active < len(snap.tabs) { | |
| 105 | + | current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.active].id} | |
| 106 | + | } | |
| 107 | + | snaps = append(snaps, snap) | |
| 108 | + | } | |
| 109 | + | sort.Slice(snaps, func(i, j int) bool { | |
| 110 | + | if snaps[i].mruAt != snaps[j].mruAt { | |
| 111 | + | return snaps[i].mruAt > snaps[j].mruAt | |
| 112 | + | } | |
| 113 | + | return snaps[i].name < snaps[j].name | |
| 112 | 114 | }) | |
| 113 | 115 | sort.Slice(stopped, func(i, j int) bool { | |
| 114 | 116 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | |
| @@ -119,50 +121,14 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||
| 119 | 121 | if pickerSortMode(d.pickerSort.Load()) == pickerSortGrouped { | |
| 120 | 122 | // Stable partition: named sessions first, ephemeral after, each keeping | |
| 121 | 123 | // its MRU order from the sort above. | |
| 122 | - | sort.SliceStable(sessions, func(i, j int) bool { | |
| 123 | - | return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral | |
| 124 | + | sort.SliceStable(snaps, func(i, j int) bool { | |
| 125 | + | return !snaps[i].ephemeral && snaps[j].ephemeral | |
| 124 | 126 | }) | |
| 125 | 127 | } | |
| 126 | 128 | ||
| 127 | - | for _, s := range sessions { | |
| 128 | - | d.refreshSessionFocusedTitles(s) | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | includeTerminalTitle := d.currentTabsConfig().TerminalTitle | |
| 132 | - | views := make([]picker.SessionView, 0, len(sessions)+len(stopped)) | |
| 133 | - | var current picker.SourceFilter | |
| 134 | - | for _, s := range sessions { | |
| 135 | - | s.mu.Lock() | |
| 136 | - | view := picker.SessionView{ | |
| 137 | - | ID: s.id, Incarnation: s.incarnation, Name: s.name, TargetName: s.name, Active: s.active, | |
| 138 | - | Tabs: make([]picker.TabEntry, 0, len(s.tabs)), | |
| 139 | - | } | |
| 140 | - | if !s.ephemeral { | |
| 141 | - | createdAt := s.createdAt | |
| 142 | - | view.ExpectedCreatedAt = &createdAt | |
| 143 | - | } | |
| 144 | - | sessionAttention := false | |
| 145 | - | for i, tb := range s.tabs { | |
| 146 | - | name := tabDisplayName(tb, i) | |
| 147 | - | tabID := domain.TabStableID(tb.stableID) | |
| 148 | - | view.Tabs = append(view.Tabs, picker.TabEntry{ | |
| 149 | - | TabID: tabID, | |
| 150 | - | Name: name, | |
| 151 | - | Detail: tabTitleDetail(name, tb.focusedPaneTitle(includeTerminalTitle)), | |
| 152 | - | Attention: tb.attention, | |
| 153 | - | }) | |
| 154 | - | if tb.attention { | |
| 155 | - | sessionAttention = true | |
| 156 | - | } | |
| 157 | - | if s == cur && i == s.active { | |
| 158 | - | current = picker.SourceFilter{Session: s.id, Incarnation: s.incarnation, TabID: tabID} | |
| 159 | - | } | |
| 160 | - | } | |
| 161 | - | if sessionAttention { | |
| 162 | - | view.Name = attentionSuffix(view.Name) | |
| 163 | - | } | |
| 164 | - | s.mu.Unlock() | |
| 165 | - | views = append(views, view) | |
| 129 | + | views := make([]picker.SessionView, 0, len(snaps)+len(stopped)) | |
| 130 | + | for _, snap := range snaps { | |
| 131 | + | views = append(views, snap.pickerView()) | |
| 166 | 132 | } | |
| 167 | 133 | for _, s := range stopped { | |
| 168 | 134 | createdAt := s.createdAt | |
| @@ -87,28 +87,30 @@ 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, name, and ephemeral once: comparators must not observe | 90 | + | |
| 91 | - | // concurrent touchMRU updates or a renameSession mutation (which mutates | 91 | + | for _, s := range sessions { |
| 92 | - | // name/ephemeral under sess.mu) mid-sort. | 92 | + | d.refreshSessionFocusedTitles(s) |
| 93 | - | type pickerSortSnapshot struct { | 93 | + | } |
| 94 | - | mruAt uint64 | 94 | + | opts := viewOptions{tabDetails: true, focusedTitles: true, terminalTitle: d.currentTabsConfig().TerminalTitle} |
| 95 | - | name string | 95 | + | |
| 96 | - | ephemeral bool | 96 | + | // One snapshot per session: sorting and view building read the same |
| 97 | - | } | 97 | + | // capture, so comparators cannot observe a concurrent touchMRU or |
| 98 | - | snap := make(map[*session]pickerSortSnapshot, len(sessions)) | 98 | + | // renameSession mid-sort (the invariant previously enforced by the |
| 99 | + | // pickerSortSnapshot map). | |||
| 100 | + | snaps := make([]sessionView, 0, len(sessions)) | |||
| 101 | + | var current picker.SourceFilter | |||
| 99 | for _, s := range sessions { | 102 | for _, s := range sessions { | ||
| 100 | - | mruAt := s.mruAt.Load() | 103 | + | snap := s.snapshotView(opts) |
| 101 | - | s.mu.Lock() | 104 | + | if s == cur && snap.active >= 0 && snap.active < len(snap.tabs) { |
| 102 | - | name := s.name | 105 | + | current = picker.SourceFilter{Session: snap.id, Incarnation: snap.incarnation, TabID: snap.tabs[snap.active].id} |
| 103 | - | ephemeral := s.ephemeral | 106 | + | } |
| 104 | - | s.mu.Unlock() | 107 | + | snaps = append(snaps, snap) |
| 105 | - | snap[s] = pickerSortSnapshot{mruAt: mruAt, name: name, ephemeral: ephemeral} | 108 | + | } |
| 106 | - | } | 109 | + | sort.Slice(snaps, func(i, j int) bool { |
| 107 | - | sort.Slice(sessions, func(i, j int) bool { | 110 | + | if snaps[i].mruAt != snaps[j].mruAt { |
| 108 | - | if snap[sessions[i]].mruAt != snap[sessions[j]].mruAt { | 111 | + | return snaps[i].mruAt > snaps[j].mruAt |
| 109 | - | return snap[sessions[i]].mruAt > snap[sessions[j]].mruAt | 112 | + | } |
| 110 | - | } | 113 | + | return snaps[i].name < snaps[j].name |
| 111 | - | return snap[sessions[i]].name < snap[sessions[j]].name | |||
| 112 | }) | 114 | }) | ||
| 113 | sort.Slice(stopped, func(i, j int) bool { | 115 | sort.Slice(stopped, func(i, j int) bool { | ||
| 114 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | 116 | if stopped[i].lastUsedSeq != stopped[j].lastUsedSeq { | ||
| @@ -119,50 +121,14 @@ func (d *Daemon) pickerViews(cur *session) ([]picker.SessionView, picker.SourceF | |||||
| 119 | if pickerSortMode(d.pickerSort.Load()) == pickerSortGrouped { | 121 | if pickerSortMode(d.pickerSort.Load()) == pickerSortGrouped { | ||
| 120 | // Stable partition: named sessions first, ephemeral after, each keeping | 122 | // Stable partition: named sessions first, ephemeral after, each keeping | ||
| 121 | // its MRU order from the sort above. | 123 | // its MRU order from the sort above. | ||
| 122 | - | sort.SliceStable(sessions, func(i, j int) bool { | 124 | + | sort.SliceStable(snaps, func(i, j int) bool { |
| 123 | - | return !snap[sessions[i]].ephemeral && snap[sessions[j]].ephemeral | 125 | + | return !snaps[i].ephemeral && snaps[j].ephemeral |
| 124 | }) | 126 | }) | ||
| 125 | } | 127 | } | ||
| 126 | 128 | ||||
| 127 | - | for _, s := range sessions { | 129 | + | views := make([]picker.SessionView, 0, len(snaps)+len(stopped)) |
| 128 | - | d.refreshSessionFocusedTitles(s) | 130 | + | for _, snap := range snaps { |
| 129 | - | } | 131 | + | views = append(views, snap.pickerView()) |
| 130 | - | ||||
| 131 | - | includeTerminalTitle := d.currentTabsConfig().TerminalTitle | |||
| 132 | - | views := make([]picker.SessionView, 0, len(sessions)+len(stopped)) | |||
| 133 | - | var current picker.SourceFilter | |||
| 134 | - | for _, s := range sessions { | |||
| 135 | - | s.mu.Lock() | |||
| 136 | - | view := picker.SessionView{ | |||
| 137 | - | ID: s.id, Incarnation: s.incarnation, Name: s.name, TargetName: s.name, Active: s.active, | |||
| 138 | - | Tabs: make([]picker.TabEntry, 0, len(s.tabs)), | |||
| 139 | - | } | |||
| 140 | - | if !s.ephemeral { | |||
| 141 | - | createdAt := s.createdAt | |||
| 142 | - | view.ExpectedCreatedAt = &createdAt | |||
| 143 | - | } | |||
| 144 | - | sessionAttention := false | |||
| 145 | - | for i, tb := range s.tabs { | |||
| 146 | - | name := tabDisplayName(tb, i) | |||
| 147 | - | tabID := domain.TabStableID(tb.stableID) | |||
| 148 | - | view.Tabs = append(view.Tabs, picker.TabEntry{ | |||
| 149 | - | TabID: tabID, | |||
| 150 | - | Name: name, | |||
| 151 | - | Detail: tabTitleDetail(name, tb.focusedPaneTitle(includeTerminalTitle)), | |||
| 152 | - | Attention: tb.attention, | |||
| 153 | - | }) | |||
| 154 | - | if tb.attention { | |||
| 155 | - | sessionAttention = true | |||
| 156 | - | } | |||
| 157 | - | if s == cur && i == s.active { | |||
| 158 | - | current = picker.SourceFilter{Session: s.id, Incarnation: s.incarnation, TabID: tabID} | |||
| 159 | - | } | |||
| 160 | - | } | |||
| 161 | - | if sessionAttention { | |||
| 162 | - | view.Name = attentionSuffix(view.Name) | |||
| 163 | - | } | |||
| 164 | - | s.mu.Unlock() | |||
| 165 | - | views = append(views, view) | |||
| 166 | } | 132 | } | ||
| 167 | for _, s := range stopped { | 133 | for _, s := range stopped { | ||
| 168 | createdAt := s.createdAt | 134 | createdAt := s.createdAt | ||
M internal/usecase/daemon/session_view.go +37 -1
| @@ -1,6 +1,9 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | - | import "github.com/bnema/vev/internal/domain" | |
| 3 | + | import ( | |
| 4 | + | "github.com/bnema/vev/internal/domain" | |
| 5 | + | "github.com/bnema/vev/internal/usecase/picker" | |
| 6 | + | ) | |
| 4 | 7 | ||
| 5 | 8 | // viewOptions controls optional (costly) fields captured by snapshotView. | |
| 6 | 9 | type viewOptions struct { | |
| @@ -82,3 +85,36 @@ func (s *session) snapshotView(opts viewOptions) sessionView { | |||
| 82 | 85 | } | |
| 83 | 86 | return view | |
| 84 | 87 | } | |
| 88 | + | ||
| 89 | + | // pickerView renders this snapshot as the picker's value type. Field-for-field | |
| 90 | + | // equivalent to the inline construction previously in pickerViews. | |
| 91 | + | func (view sessionView) pickerView() picker.SessionView { | |
| 92 | + | out := picker.SessionView{ | |
| 93 | + | ID: view.id, | |
| 94 | + | Incarnation: view.incarnation, | |
| 95 | + | Name: view.name, | |
| 96 | + | TargetName: view.name, | |
| 97 | + | Active: view.active, | |
| 98 | + | Tabs: make([]picker.TabEntry, 0, len(view.tabs)), | |
| 99 | + | } | |
| 100 | + | if !view.ephemeral { | |
| 101 | + | createdAt := view.createdAt | |
| 102 | + | out.ExpectedCreatedAt = &createdAt | |
| 103 | + | } | |
| 104 | + | attention := false | |
| 105 | + | for _, tb := range view.tabs { | |
| 106 | + | out.Tabs = append(out.Tabs, picker.TabEntry{ | |
| 107 | + | TabID: tb.id, | |
| 108 | + | Name: tb.name, | |
| 109 | + | Detail: tabTitleDetail(tb.name, tb.focusedTitle), | |
| 110 | + | Attention: tb.attention, | |
| 111 | + | }) | |
| 112 | + | if tb.attention { | |
| 113 | + | attention = true | |
| 114 | + | } | |
| 115 | + | } | |
| 116 | + | if attention { | |
| 117 | + | out.Name = attentionSuffix(out.Name) | |
| 118 | + | } | |
| 119 | + | return out | |
| 120 | + | } | |
| @@ -1,6 +1,9 @@ | |||||
| 1 | package daemon | 1 | package daemon | ||
| 2 | 2 | ||||
| 3 | - | import "github.com/bnema/vev/internal/domain" | 3 | + | import ( |
| 4 | + | "github.com/bnema/vev/internal/domain" | |||
| 5 | + | "github.com/bnema/vev/internal/usecase/picker" | |||
| 6 | + | ) | |||
| 4 | 7 | ||||
| 5 | // viewOptions controls optional (costly) fields captured by snapshotView. | 8 | // viewOptions controls optional (costly) fields captured by snapshotView. | ||
| 6 | type viewOptions struct { | 9 | type viewOptions struct { | ||
| @@ -82,3 +85,36 @@ func (s *session) snapshotView(opts viewOptions) sessionView { | |||||
| 82 | } | 85 | } | ||
| 83 | return view | 86 | return view | ||
| 84 | } | 87 | } | ||
| 88 | + | ||||
| 89 | + | // pickerView renders this snapshot as the picker's value type. Field-for-field | |||
| 90 | + | // equivalent to the inline construction previously in pickerViews. | |||
| 91 | + | func (view sessionView) pickerView() picker.SessionView { | |||
| 92 | + | out := picker.SessionView{ | |||
| 93 | + | ID: view.id, | |||
| 94 | + | Incarnation: view.incarnation, | |||
| 95 | + | Name: view.name, | |||
| 96 | + | TargetName: view.name, | |||
| 97 | + | Active: view.active, | |||
| 98 | + | Tabs: make([]picker.TabEntry, 0, len(view.tabs)), | |||
| 99 | + | } | |||
| 100 | + | if !view.ephemeral { | |||
| 101 | + | createdAt := view.createdAt | |||
| 102 | + | out.ExpectedCreatedAt = &createdAt | |||
| 103 | + | } | |||
| 104 | + | attention := false | |||
| 105 | + | for _, tb := range view.tabs { | |||
| 106 | + | out.Tabs = append(out.Tabs, picker.TabEntry{ | |||
| 107 | + | TabID: tb.id, | |||
| 108 | + | Name: tb.name, | |||
| 109 | + | Detail: tabTitleDetail(tb.name, tb.focusedTitle), | |||
| 110 | + | Attention: tb.attention, | |||
| 111 | + | }) | |||
| 112 | + | if tb.attention { | |||
| 113 | + | attention = true | |||
| 114 | + | } | |||
| 115 | + | } | |||
| 116 | + | if attention { | |||
| 117 | + | out.Name = attentionSuffix(out.Name) | |||
| 118 | + | } | |||
| 119 | + | return out | |||
| 120 | + | } | |||