From 54201ea65a3501d09a87c776376a0c16aa39db70 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Sun, 1 Nov 2026 15:19:41 +0000 Subject: [PATCH] Add naming and navigation helpers for TUI backend sessions (#58722) ## What changed Add `BackendSessions` helpers to name the displayed listening backend, find the first listening backend, and cycle through listening backends in registration order. Cycling wraps around, skips closed streams, and excludes the current backend. Display names use `Local` for the initial session and `Backend N` for additional sessions; a closed displayed stream has no name. ## Testing Extend backend session tests to cover cycling with a single backend, wraparound across three backends, and skipping a closed stream. GitOrigin-RevId: e8f3fcc38a178b6b91d385d65d899fdd042062c5 --- codex-rs/tui/src/app/backend_sessions.rs | 28 +++++++++++++++++++ .../src/app/tests/backend_sessions_tests.rs | 14 +++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app/backend_sessions.rs b/codex-rs/tui/src/app/backend_sessions.rs index 5112df4530..2795b4824e 100644 --- a/codex-rs/tui/src/app/backend_sessions.rs +++ b/codex-rs/tui/src/app/backend_sessions.rs @@ -118,6 +118,34 @@ impl BackendSessions { .collect() } + #[expect(dead_code, reason = "used by the next stack stage")] + pub(super) fn displayed_name(&self) -> Option { + let slot = self.displayed.0; + self.entries[slot].listening.then(|| match slot { + 0 => "Local".to_string(), + index => format!("Backend {}", index + 1), + }) + } + + #[expect(dead_code, reason = "used by the next stack stage")] + pub(super) fn first_listening_backend(&self) -> Option { + self.entries + .iter() + .find(|entry| entry.listening) + .map(|entry| entry.session.backend_id()) + } + + /// Cycle through listening connections in registration order, excluding the current one. + #[cfg_attr(not(test), expect(dead_code, reason = "used by the next stack stage"))] + pub(super) fn next_listening_backend(&self, current: BackendId) -> Option { + let index = self.slot(current)?.0; + self.entries[index + 1..] + .iter() + .chain(&self.entries[..index]) + .find(|entry| entry.listening) + .map(|entry| entry.session.backend_id()) + } + pub(super) fn session_at_mut(&mut self, slot: SessionSlot) -> &mut AppServerSession { &mut self.entries[slot.0].session } diff --git a/codex-rs/tui/src/app/tests/backend_sessions_tests.rs b/codex-rs/tui/src/app/tests/backend_sessions_tests.rs index 9dcf00d9a7..23a01e84ab 100644 --- a/codex-rs/tui/src/app/tests/backend_sessions_tests.rs +++ b/codex-rs/tui/src/app/tests/backend_sessions_tests.rs @@ -43,7 +43,10 @@ async fn backend_sessions_parked_request_does_not_hide_disconnect() -> Result<() ) }); let background_id = background.backend_id(); - let mut backends = BackendSessions::from_initial(displayed, vec![background]); + let displayed_id = displayed.backend_id(); + let mut backends = BackendSessions::new(displayed); + assert_eq!(backends.next_listening_backend(displayed_id), None); + backends.insert(background); let initial = backends.displayed; let thread_id = ThreadId::new(); @@ -185,6 +188,13 @@ async fn backend_sessions_poll_three_sources_and_isolate_closed_or_paused_stream } let mut sessions = sessions.into_iter(); let mut backends = BackendSessions::from_initial(sessions.next().unwrap(), sessions.collect()); + assert_eq!( + owners + .iter() + .map(|owner| backends.next_listening_backend(*owner)) + .collect::>(), + [Some(owners[1]), Some(owners[2]), Some(owners[0])] + ); let mut unpaused = HashSet::new(); for _ in 0..2 { let (owner, event) = @@ -221,6 +231,8 @@ async fn backend_sessions_poll_three_sources_and_isolate_closed_or_paused_stream } } assert!(!backends.is_listening(owners[0])); + assert_eq!(backends.next_listening_backend(owners[2]), Some(owners[1])); + assert_eq!(backends.next_listening_backend(owners[0]), Some(owners[1])); for index in 1..3 { assert!(backends.is_listening(owners[index])); let _: ThreadSettingsUpdateResponse = backends