1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
//! What order the picker's menu shows, and the numbers it prints beside each
//! row (#3483, #3723, #6753).
//!
//! Why: split out of `session_picker.rs` when #6753 pushed that file past the
//! 500-SLOC production cap. The cut is a domain rather than a size convenience:
//! everything here answers "which row comes first and what number does it
//! carry", which is exactly the question #6753 found two different answers to —
//! the picker's first menu was ordered by the daemon's ascending slot and every
//! later one by the scope. [`prepare_menu`] is now the single answer, and living
//! in its own module is what keeps a second one from being added beside it.
//! What: the scope filter, the attached-then-active-then-rest sort, the stable
//! slot numbering with its positional fallback, and [`prepare_menu`], which
//! composes them into the whole state one render is decided by.
//! Test: `session_picker_tests.rs` — `prepare_menu_*`, `sort_sessions_*`,
//! `filter_sessions_by_term_*`, `next_launch_slot_*`.
use ManagedSessionSummary;
use ;
/// Apply a [`SessionFilter`] to a session list (#3483).
///
/// Why: the static table and the interactive picker must filter identically, so
/// both call this rather than open-coding the predicate.
/// What: keeps the sessions [`SessionFilter::matches`] accepts. `filter = None`
/// is a no-op (returns `sessions` unchanged).
/// Test: `filter_sessions_by_term_matches_name`,
/// `filter_sessions_by_term_matches_task`,
/// `filter_sessions_by_term_matches_source_id`,
/// `filter_sessions_by_term_is_case_insensitive`,
/// `filter_sessions_by_term_no_match_returns_empty`,
/// `filter_sessions_by_term_none_is_noop`,
/// `filter_sessions_by_name_ignores_non_name_columns`.
pub
/// Put a freshly-obtained session list into the order and scope the picker
/// renders (#6753).
///
/// Why: [`run_tty_picker`](super::session_picker::run_tty_picker) applied [`filter_sessions_by_term`] and
/// [`sort_sessions`] at the BOTTOM of its loop only, so its FIRST menu showed
/// the daemon's ascending-slot order while every later menu showed the scope's
/// order. The attached session — usually the highest slot — printed last, bare
/// Enter therefore targeted the oldest session while the hint read "resume most
/// recent", and the whole list reordered under the operator after the first
/// action. `tm ls`'s connector normalized before its first render and the bare
/// `tm` path did not, which is the divergence this function removes: it is now
/// the ONE place a list is prepared, applied at the TOP of the loop, so the
/// first render and every later one are the same code path rather than two that
/// must be kept in step.
/// What: the scope's term filter, then the scope's sort. Both are idempotent, so
/// a caller that already normalized (the `tm ls` connector) is unaffected.
/// Test: `prepare_menu_orders_the_first_render_like_every_later_one`,
/// `prepare_menu_is_idempotent`, `prepare_menu_applies_the_scopes_term_filter`.
pub
/// Everything one render of the picker menu is decided by (#6753).
///
/// Why: the three values below were computed inline at the top of
/// [`run_tty_picker`](super::session_picker::run_tty_picker)'s loop, from whatever order `sessions` happened to be in.
/// That made the ORDER an input nothing named and nothing could test, which is
/// how the first render came to disagree with every later one. Naming the
/// prepared list and its derived values together means a render cannot be
/// produced without going through [`prepare_menu`], so there is no second path
/// for the order to diverge on.
/// What: the normalized list plus the three menu-wide facts derived from it.
/// Test: `prepare_menu_*` in `session_picker_tests.rs`.
pub
/// Prepare one render of the picker menu from a freshly-obtained list (#6753).
///
/// Why: this is the ONE place a menu is derived, so the first menu and every
/// later one are the same computation rather than two that must be kept in step.
/// [`run_tty_picker`](super::session_picker::run_tty_picker) used to normalize at the BOTTOM of its loop only: its first
/// menu therefore printed the daemon's ascending-slot order, in which the
/// attached session — usually the highest slot — came LAST. Bare Enter targets
/// position 0, so it aimed at the oldest session (usually stopped, so it fired
/// `ConfirmRestart`) while the hint read "resume most recent", and the whole list
/// reordered under the operator after the first action.
/// What: [`normalize_for_scope`] first, then [`slots_are_stale`],
/// [`next_launch_slot`], the #3552 pinned `default_idx`, and the
/// `first_needs_restart` flag — all off the NORMALIZED list, and the last two
/// off `default_idx` rather than position 0 so the restart gate follows
/// whichever row bare Enter will actually hit. A deleted default row is never
/// `first_needs_restart`, because
/// [`decide_for_index`](super::session_picker::decide_for_index) checks `deleted` ahead of this flag.
/// Test: `prepare_menu_orders_the_first_render_like_every_later_one`,
/// `prepare_menu_bare_enter_targets_the_attached_session`,
/// `prepare_menu_is_idempotent`, `prepare_menu_applies_the_scopes_term_filter`,
/// `prepare_menu_launch_slot_is_the_maximum_not_the_last`,
/// `prepare_menu_keeps_the_pinned_default_across_a_resort`.
pub
/// The attached→active→everything-else group a session belongs in (owner
/// request 2026-07-29).
///
/// Why: Bob's ask — the listing should group attached sessions first, then
/// active ones, then the rest (stopped/errored/provisioning/etc.) — ABOVE
/// whatever `recent`/`alpha` secondary order the operator picked, so a
/// session they're actively connected to never scrolls below a merely-recent
/// stopped one.
/// What: `0` when `s.attached` (a client is connected RIGHT NOW — the
/// strongest signal, mirrors [`session_picker_render::state_color`](crate::commands::session_picker_render::state_color)'s own
/// precedence); `1` for `state == "active"` (not attached); `2` for every
/// other state. Lower sorts first.
/// Test: `sort_sessions_recent_groups_attached_before_active_before_stopped`,
/// `sort_sessions_alpha_groups_attached_before_active_before_stopped`.
// #7421: also ranks a PROJECT for the `tm ls` new-session picker, through
// `session_tui::new_session_order`, so both surfaces share one precedence.
pub
/// Sort `sessions` in place per [`SessionSortArg`] (#3483), grouped
/// attached→active→everything-else (owner request 2026-07-29).
///
/// Why: shared by the static table (`tm ls` / `tm sessions ls`) and the
/// interactive picker so both views order sessions identically.
/// What: primary key is [`group_rank`] (attached, then active, then the
/// rest); within each group, `Recent` sorts descending by [`recency_key`]
/// (most recent first) and `Alpha` sorts ascending, case-insensitively, by
/// `name`. Both use the stable `sort_by`, so equal keys preserve the
/// daemon's original relative order.
/// Test: `sort_sessions_recent_orders_by_last_activity`,
/// `sort_sessions_recent_falls_back_to_created_at`,
/// `sort_sessions_alpha_orders_by_name_case_insensitive`,
/// `sort_sessions_recent_groups_attached_before_active_before_stopped`,
/// `sort_sessions_alpha_groups_attached_before_active_before_stopped`.
pub
/// Best-available recency signal for a session (#3483).
///
/// Why: `last_activity_at` reflects actual usage (the daemon updates it on
/// every interaction), which is the signal an operator scanning `tm ls`
/// actually wants — a session touched five minutes ago should outrank one
/// merely CREATED first. `created_at` is the fallback for legacy/additive
/// records that predate the activity timestamp; a session with neither sorts
/// last (empty string is the lexicographic minimum).
/// What: RFC 3339 timestamps compare correctly as plain strings because the
/// daemon always emits them in the same normalized (UTC, fixed-precision)
/// form.
/// Test: covered indirectly by `sort_sessions_recent_orders_by_last_activity`
/// and `sort_sessions_recent_falls_back_to_created_at`.
// #7421: shared with `session_tui::new_session_order`, which ranks a project by
// the newest key among its sessions.
pub