Keep TUI session selections bound to their backend (#54681)

## Why

A thread ID alone does not identify the backend that owns a session. Resume and fork selections must retain that ownership across connection replacement so they cannot operate on another backend, even when thread IDs match.

## What changed

- Add a process-local `BackendId` to app-server sessions and session targets, preserving it across reconnects and auxiliary picker connections.
- Reject selections from a different backend before changing session state or sending resume, fork, or unarchive requests.
- Disable local-daemon fallback to an embedded server for auxiliary pickers and after selecting a session, while retaining fallback for fresh startup.

## Testing

Add regression coverage for foreign selections with matching thread IDs, startup resume and fork rejection before requests, shared ownership across picker connections, and fallback eligibility.

GitOrigin-RevId: 05a3ca0a901a8dc24337d130548aa2c048b4d586
This commit is contained in:
Benjamin Carlsson
2026-10-16 19:13:40 +00:00
committed by copyberry
parent a083a1e49e
commit 440312f113
52 changed files with 643 additions and 37 deletions
+1
View File
@@ -551,6 +551,7 @@ impl App {
(config.clone(), self.local_settings.reloaded(config))
} else if unloaded {
let target_session = SessionTarget {
backend_id: app_server.backend_id(),
path: target_thread.path.clone(),
thread_id: root_thread_id,
cwd: Some(target_thread.cwd.to_path_buf()),
@@ -584,6 +584,7 @@ async fn hiding_rename_target_does_not_transfer_draft_to_neighbor() -> Result<()
async fn hidden_task_stays_hidden_through_activity_and_seed_until_explicit_resume() -> Result<()> {
let (mut app, mut rx, _op_rx) = crate::app::tests::make_test_app_with_channels().await;
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let backend_id = app_server.backend_id();
let started = app_server.start_thread(&app.config).await?;
let id = started.session.thread_id;
app.enqueue_primary_thread_session(started.session, started.turns)
@@ -646,6 +647,7 @@ async fn hidden_task_stays_hidden_through_activity_and_seed_until_explicit_resum
&mut tui,
&mut app_server,
SessionTarget {
backend_id,
thread_id: id,
path: None,
cwd: None,
@@ -143,6 +143,7 @@ async fn check_discovery(mixed_sources: bool) -> Result<()> {
Ok::<_, color_eyre::Report>(cursors)
});
let session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
+10 -1
View File
@@ -1910,10 +1910,12 @@ async fn overview_selection_applies_user_permissions_only_to_unloaded_threads()
// The picker must also abort when a session-only choice violates destination policy.
std::fs::write(&requirements, "allowed_approval_policies = [\"never\"]\n")?;
app.harness_overrides.approval_policy = Some(codex_protocol::protocol::AskForApproval::Never);
let backend_id = app_server.backend_id();
Box::pin(app.resume_target_session(
&mut tui,
&mut app_server,
SessionTarget {
backend_id,
path: None,
thread_id: thread_ids[4],
cwd: None,
@@ -2263,11 +2265,13 @@ async fn resuming_active_session_closes_command_center() -> Result<()> {
app.chat_widget.show_bottom_pane_view(Box::new(view));
let mut tui = crate::tui::test_support::make_test_tui()?;
let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = server.backend_id();
assert!(matches!(
app.apply_resume_picker_selection(
&mut tui,
&mut server,
SessionSelection::Resume(SessionTarget {
backend_id,
path: None,
thread_id,
cwd: None,
@@ -2309,6 +2313,8 @@ async fn resume_picker_round_trip_preserves_each_threads_input() -> Result<()> {
app.config.codex_home.join("config.toml"),
"[tui]\nresume_cwd = \"current\"\n",
)?;
let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = server.backend_id();
let mut targets = Vec::new();
for (timestamp, name) in [
("2025-01-05T12-00-00", "First task"),
@@ -2324,6 +2330,7 @@ async fn resume_picker_round_trip_preserves_each_threads_input() -> Result<()> {
)
.expect("saved rollout");
targets.push(SessionTarget {
backend_id,
path: Some(app_test_support::rollout_path(
app.config.codex_home.as_path(),
timestamp,
@@ -2335,7 +2342,6 @@ async fn resume_picker_round_trip_preserves_each_threads_input() -> Result<()> {
});
}
let mut tui = crate::tui::test_support::make_test_tui()?;
let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let mut expected_states = Vec::new();
for target in targets.iter().chain(&targets) {
let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None);
@@ -2425,11 +2431,13 @@ async fn command_center_handles_resume_failure_and_success() -> Result<()> {
app.chat_widget.show_bottom_pane_view(Box::new(view));
let mut tui = crate::tui::test_support::make_test_tui()?;
let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = server.backend_id();
assert!(matches!(
Box::pin(app.apply_resume_picker_selection(
&mut tui,
&mut server,
SessionSelection::Resume(SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -2454,6 +2462,7 @@ async fn command_center_handles_resume_failure_and_success() -> Result<()> {
&mut tui,
&mut server,
SessionSelection::Resume(SessionTarget {
backend_id,
path: Some(path),
thread_id,
cwd: None,
@@ -1387,6 +1387,7 @@ mod tests {
)
.expect("write ChatGPT authentication");
let app_server = crate::start_app_server_for_picker(
crate::backend_id::BackendId::new(),
&app.config,
&crate::AppServerTarget::Embedded,
app.cli_kv_overrides.clone(),
@@ -39,6 +39,7 @@ async fn experimental_features_use_selected_server_profile_and_preserve_task_set
))
.await?;
let mut server = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Remote,
);
+1
View File
@@ -435,6 +435,7 @@ impl App {
&& let Some(thread_id) = self.current_displayed_thread_id()
{
let target = SessionTarget {
backend_id: app_server.backend_id(),
path: self
.primary_session_configured
.as_ref()
+6 -1
View File
@@ -27,7 +27,12 @@ pub(super) struct Reconnected {
thread: Option<AppServerStartedThread>,
}
#[expect(
clippy::too_many_arguments,
reason = "retain logical ownership across connection replacement"
)]
pub(super) async fn reconnect(
backend_id: crate::backend_id::BackendId,
target: AppServerTarget,
config: Config,
local_settings: crate::local_settings::LocalSettings,
@@ -55,7 +60,7 @@ pub(super) async fn reconnect(
let attempt = async {
tokio::time::sleep(Duration::from_secs(delay)).await;
let client = crate::app_server_connection::connect(&target).await?;
let mut session = AppServerSession::new(client, mode)
let mut session = AppServerSession::new(backend_id, client, mode)
.with_local_codex_home(&config.codex_home)
.with_remote_cwd_override(remote_cwd.clone())
.with_thread_tool_transport(task_tools.clone());
+4
View File
@@ -32,6 +32,10 @@ impl App {
app_server: &mut AppServerSession,
target_session: &SessionTarget,
) -> std::result::Result<(Config, crate::local_settings::LocalSettings), AppRunControl> {
if let Err(error) = target_session.ensure_backend(app_server.backend_id()) {
self.add_session_picker_error(error.to_string());
return Err(AppRunControl::Continue);
}
self.refresh_in_memory_config_from_disk_best_effort("resuming a thread")
.await;
let cwd_override = self
+5 -1
View File
@@ -60,7 +60,11 @@ async fn connection() -> color_eyre::Result<(
});
let client = crate::connect_remote_app_server(endpoint).await?;
Ok((
AppServerSession::new(client, ThreadParamsMode::Remote),
AppServerSession::new(
crate::backend_id::BackendId::new(),
client,
ThreadParamsMode::Remote,
),
receiver,
task,
))
@@ -1322,6 +1322,10 @@ impl App {
app_server: &mut AppServerSession,
target_session: SessionTarget,
) -> Result<AppRunControl> {
if let Err(error) = target_session.ensure_backend(app_server.backend_id()) {
self.add_session_picker_error(error.to_string());
return Ok(AppRunControl::Continue);
}
if self.ignore_same_thread_resume(&target_session) {
self.agents_overview
.hidden_threads
+6
View File
@@ -11,6 +11,7 @@ impl App {
) -> Result<AppRunControl> {
// Keep embedded-server initialization on its own runtime task as well as opening the
// picker on a fresh event-loop iteration; its auth manager needs more stack headroom.
let backend_id = app_server.backend_id();
let picker_config = self.config.clone();
let picker_target = self.app_server_target.clone();
let picker_cli_kv_overrides = self.cli_kv_overrides.clone();
@@ -19,6 +20,7 @@ impl App {
let picker_environment_manager = Arc::clone(&self.environment_manager);
let picker_app_server = tokio::spawn(async move {
crate::start_app_server_for_picker(
backend_id,
&picker_config,
&picker_target,
picker_cli_kv_overrides,
@@ -82,6 +84,10 @@ impl App {
) -> Result<AppRunControl> {
match selection {
SessionSelection::Resume(target_session) => {
if let Err(error) = target_session.ensure_backend(app_server.backend_id()) {
self.add_session_picker_error(error.to_string());
return Ok(AppRunControl::Continue);
}
let thread_id = target_session.thread_id;
let switching_threads = self.current_displayed_thread_id() != Some(thread_id);
if switching_threads {
+6
View File
@@ -270,6 +270,11 @@ impl App {
tui.prepare_owned_screen(config.tui_fullscreen_transcript)?;
let mut local_settings = crate::local_settings::LocalSettings::for_tui(&config, tui);
let startup_started_at = Instant::now();
if let SessionSelection::Resume(target) | SessionSelection::Fork(target) =
&session_selection
{
target.ensure_backend(app_server.backend_id())?;
}
let (app_event_tx, mut app_event_rx) = unbounded_channel();
let app_event_tx = AppEventSender::new(app_event_tx);
if let Some(message) = project_config_warning(&config) {
@@ -1243,6 +1248,7 @@ See the Codex keymap documentation for supported actions and examples."
}
if app.reconnect.offline && !app.reconnect.failed && reconnect.is_none() {
reconnect = Some(Box::pin(reconnect::reconnect(
app_server.backend_id(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
+16
View File
@@ -3598,6 +3598,7 @@ async fn remote_resume_rejects_explicit_permission_override() -> Result<()> {
)?;
app.config.codex_home = home.path().to_path_buf().abs();
let mut server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let backend_id = server.backend_id();
app.cli_kv_overrides
.push(("default_permissions".into(), ":workspace".into()));
assert!(
@@ -3618,6 +3619,7 @@ async fn remote_resume_rejects_explicit_permission_override() -> Result<()> {
&mut tui,
&mut server,
SessionTarget {
backend_id,
thread_id: ThreadId::new(),
path: None,
cwd: None,
@@ -8121,6 +8123,7 @@ async fn remote_resume_current_cwd_rejection_snapshot() -> Result<()> {
},
};
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = app_server.backend_id();
let mut tui = crate::tui::test_support::make_test_tui()?;
let control = app
@@ -8128,6 +8131,7 @@ async fn remote_resume_current_cwd_rejection_snapshot() -> Result<()> {
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -8165,6 +8169,7 @@ async fn remote_exec_resume_current_cwd_is_rejected() -> Result<()> {
.await,
);
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = app_server.backend_id();
let mut tui = crate::tui::test_support::make_test_tui()?;
let control = app
@@ -8172,6 +8177,7 @@ async fn remote_exec_resume_current_cwd_is_rejected() -> Result<()> {
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -8204,6 +8210,7 @@ async fn in_app_resume_session_cwd_without_metadata_is_non_fatal() -> Result<()>
app.state_db = None;
let active_thread_id = app.chat_widget.thread_id();
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = app_server.backend_id();
let mut tui = crate::tui::test_support::make_test_tui()?;
let control = app
@@ -8211,6 +8218,7 @@ async fn in_app_resume_session_cwd_without_metadata_is_non_fatal() -> Result<()>
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -8267,6 +8275,7 @@ async fn remote_resume_keeps_server_only_cwd_out_of_local_config() -> Result<()>
};
app.harness_overrides.cwd = Some(remote_cwd.clone());
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
let backend_id = app_server.backend_id();
Box::pin(app_server.resume_thread(
&crate::local_settings::LocalSettings::from(&app.config),
app.config.clone(),
@@ -8290,6 +8299,7 @@ async fn remote_resume_keeps_server_only_cwd_out_of_local_config() -> Result<()>
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: Some(rollout_path),
thread_id: ThreadId::from_string(&thread_id)?,
cwd: None,
@@ -8423,6 +8433,7 @@ async fn in_app_resume_uses_configured_or_explicit_cwd() -> Result<()> {
Some(server) => server,
slot @ None => slot.insert(
crate::start_app_server_for_picker(
crate::backend_id::BackendId::new(),
&config,
&crate::AppServerTarget::Embedded,
Vec::new(),
@@ -8433,6 +8444,7 @@ async fn in_app_resume_uses_configured_or_explicit_cwd() -> Result<()> {
.await?,
),
};
let backend_id = app_server.backend_id();
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
app.config = config;
app.launch_cwd = launch_cwd.clone();
@@ -8448,6 +8460,7 @@ async fn in_app_resume_uses_configured_or_explicit_cwd() -> Result<()> {
&mut tui,
app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: Some(rollout_path),
thread_id,
cwd: None,
@@ -8519,6 +8532,7 @@ async fn remembered_current_cwd_stays_at_launch_across_in_app_resumes() -> Resul
.await
.map_err(std::io::Error::other)?;
let backend_id = crate::backend_id::BackendId::new();
let mut targets = Vec::new();
for (filename_timestamp, metadata_timestamp, session_cwd) in [
(
@@ -8560,6 +8574,7 @@ async fn remembered_current_cwd_stays_at_launch_across_in_app_resumes() -> Resul
),
)?;
targets.push(crate::resume_picker::SessionTarget {
backend_id,
path: Some(rollout_path),
thread_id: ThreadId::from_string(&thread_id)?,
cwd: None,
@@ -8570,6 +8585,7 @@ async fn remembered_current_cwd_stays_at_launch_across_in_app_resumes() -> Resul
crate::init_state_db_for_app_server_target(&config, &crate::AppServerTarget::Embedded)
.await?;
let mut app_server = Box::pin(crate::start_app_server_for_picker(
backend_id,
&config,
&crate::AppServerTarget::Embedded,
Vec::new(),
@@ -295,7 +295,9 @@ async fn reconnect_restores_history_permissions_and_resumes_unsent_input() -> Re
.unwrap()
.creating_worktree = true;
let old_sender = app.app_event_tx.clone();
let backend_id = session.backend_id();
let connected = reconnect(
session.backend_id(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
@@ -312,6 +314,7 @@ async fn reconnect_restores_history_permissions_and_resumes_unsent_input() -> Re
);
app.finish_reconnect(&mut tui, &mut session, &mut events, connected, "2.1.0")
.await?;
assert_eq!(session.backend_id(), backend_id);
if edit_offline {
assert!(!app.chat_widget.has_active_view());
assert!(
@@ -568,6 +571,7 @@ async fn reconnect_reconciles_offscreen_pending_profile_before_restoring_permiss
let mut tui = crate::tui::test_support::make_test_tui()?;
app.begin_reconnect();
let connected = reconnect(
session.backend_id(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
@@ -641,6 +645,7 @@ async fn reconnect_exhaustion_and_unknown_initial_thread_stay_offline() -> Resul
for id in [Some(ThreadId::new()), None] {
assert!(
reconnect(
crate::backend_id::BackendId::new(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
@@ -691,6 +696,7 @@ async fn reconnect_allows_slow_hydration_but_bounds_a_stalled_server() -> Result
});
let start = tokio::time::Instant::now();
let result = reconnect(
crate::backend_id::BackendId::new(),
AppServerTarget::Remote {
endpoint: endpoint.clone(),
},
@@ -328,6 +328,7 @@ async fn backend_banner_fallback_handles_settings_failure_and_queued_manual_sele
})
.await?;
let mut server = AppServerSession::new(
crate::backend_id::BackendId::new(),
client,
crate::app_server_session::ThreadParamsMode::Embedded,
);
@@ -0,0 +1,275 @@
//! Selection must retain backend provenance even when native thread IDs collide.
use super::start_recording_app_server;
use super::*;
use crate::app_server_session::ResumeModelSettings;
use crate::backend_id::BackendId;
use crate::session_start::SessionStartAction;
use crate::session_start::SessionStartConfig;
use crate::session_start::complete_session_start;
use codex_app_server_protocol::ThreadHistoryMode;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn foreign_backend_selection_cannot_resume_same_id_or_unarchive() -> Result<()> {
let (mut app, mut events, _) = make_test_app_with_channels().await;
let mut tui = crate::tui::test_support::make_test_tui()?;
let (mut server, requests, proxy) = start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
)
.await?;
let origin = crate::start_embedded_app_server_for_picker(&app.config).await?;
let thread_id = ThreadId::new();
app.enqueue_primary_thread_session(
test_thread_session(thread_id, app.config.cwd.to_path_buf()),
Vec::new(),
)
.await?;
app.chat_widget.insert_str("keep this draft");
let target = SessionTarget {
backend_id: origin.backend_id(),
thread_id,
path: None,
cwd: Some(test_path_buf("/wrong-backend")),
history_mode: Some(ThreadHistoryMode::Legacy),
};
requests.lock().unwrap().clear();
while events.try_recv().is_ok() {}
app.apply_resume_picker_selection(
&mut tui,
&mut server,
SessionSelection::Resume(target.clone()),
)
.await?;
app.resume_target_session(&mut tui, &mut server, target.clone())
.await?;
assert_eq!(app.current_displayed_thread_id(), Some(thread_id));
assert_eq!(
app.chat_widget.composer_text_with_pending(),
"keep this draft"
);
let errors: Vec<_> = std::iter::from_fn(|| events.try_recv().ok())
.filter_map(|event| {
match event {
AppEvent::InsertHistoryCell(cell) => {
Some(lines_to_single_string(&cell.display_lines(/*width*/ 100)))
}
_ => None,
}
})
.collect();
insta::assert_snapshot!(errors.join("\n"), @r###"
■ This session belongs to a different backend. Open the session picker again.
■ This session belongs to a different backend. Open the session picker again.
"###);
for action in [
SessionStartAction::Resume(
ResumeModelSettings::RestoreFromThread,
crate::resume_permissions::ResumePermissions::default(),
),
SessionStartAction::Fork(crate::app_server_session::ForkPermissionMode::InheritSaved),
] {
let result = complete_session_start(
&mut server,
SessionStartConfig {
config: &app.config,
local_settings: &app.local_settings,
},
&AppServerTarget::Embedded,
&target,
action,
Err(color_eyre::eyre::eyre!(
"session {thread_id} is archived. Run `codex unarchive {thread_id}`"
)),
async || panic!("foreign targets must not ask to unarchive"),
)
.await;
assert!(
result
.unwrap_err()
.to_string()
.contains("different backend")
);
}
assert!(
requests.lock().unwrap().is_empty(),
"a foreign selection must cause zero requests"
);
origin.shutdown().await?;
server.shutdown().await?;
proxy.await??;
Ok(())
}
#[tokio::test]
async fn startup_fallback_requires_no_established_backend() -> Result<()> {
let app = make_test_app().await;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let address = listener.local_addr()?;
drop(listener);
let daemon_target = AppServerTarget::LocalDaemon {
endpoint: crate::RemoteAppServerEndpoint::WebSocket {
websocket_url: format!("ws://{address}"),
auth_token: None,
},
allow_embedded_fallback: true,
};
let selected = SessionTarget {
backend_id: BackendId::new(),
thread_id: ThreadId::new(),
path: None,
cwd: None,
history_mode: None,
};
for selection in [
SessionSelection::Resume(selected.clone()),
SessionSelection::Fork(selected),
SessionSelection::StartFresh,
] {
let mut target = daemon_target.clone();
target.preserve_selected_backend(&selection);
let loader = LoaderOverrides::without_managed_config_for_tests();
let policy = codex_app_server_client::EmbeddedNetworkPolicy::load(&loader).await;
let mut state_db = None;
let result = crate::start_app_server(
&mut target,
Default::default(),
app.config.clone(),
Vec::new(),
loader,
/*strict_config*/ false,
crate::CloudConfigBundleLoader::default(),
codex_feedback::CodexFeedback::new(),
/*log_db*/ None,
&mut state_db,
Arc::clone(&app.environment_manager),
policy,
)
.await;
if matches!(selection, SessionSelection::StartFresh) {
let client = result?;
assert_eq!(target, AppServerTarget::Embedded);
client.shutdown().await?;
} else {
assert!(
result.is_err(),
"a selected session must not change backend"
);
}
}
let error = crate::start_app_server_for_picker(
BackendId::new(),
&app.config,
&daemon_target,
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
/*state_db*/ None,
Arc::clone(&app.environment_manager),
)
.await
.err()
.expect("an auxiliary picker must not switch backends");
assert!(
error
.to_string()
.contains(crate::daemon_startup::FAILURE_HINT)
);
Ok(())
}
#[tokio::test]
async fn auxiliary_picker_connection_preserves_backend_selection() -> Result<()> {
let mut app = make_test_app().await;
let backend_id = BackendId::new();
let mut sessions = Vec::new();
for _ in 0..2 {
sessions.push(
crate::start_app_server_for_picker(
backend_id,
&app.config,
&AppServerTarget::Embedded,
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
/*state_db*/ None,
Arc::clone(&app.environment_manager),
)
.await?,
);
}
let mut consumer = sessions.pop().unwrap();
let mut picker = sessions.pop().unwrap();
let id = app_test_support::create_fake_rollout(
app.config.codex_home.as_path(),
"2026-01-01T00-00-00",
"2026-01-01T00:00:00Z",
"backend-owned session",
Some(&app.config.model_provider_id),
/*git_info*/ None,
)
.expect("saved rollout");
let target = crate::lookup_session_target_with_app_server(&mut picker, &app.config, &id)
.await?
.expect("picker target");
assert_eq!(target.backend_id, consumer.backend_id());
crate::legacy_core::config::set_project_trust_level(
app.config.codex_home.as_path(),
app.config.cwd.as_path(),
codex_protocol::config_types::TrustLevel::Trusted,
)
.map_err(std::io::Error::other)?;
app.harness_overrides.cwd = Some(app.config.cwd.to_path_buf());
let thread_id = target.thread_id;
let mut tui = crate::tui::test_support::make_test_tui()?;
app.resume_target_session(&mut tui, &mut consumer, target)
.await?;
assert_eq!(app.chat_widget.thread_id(), Some(thread_id));
picker.shutdown().await?;
consumer.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn startup_rejects_foreign_resume_and_fork_before_requests() -> Result<()> {
let app = make_test_app().await;
let target = SessionTarget {
backend_id: BackendId::new(),
thread_id: ThreadId::new(),
path: None,
cwd: None,
history_mode: None,
};
for selection in [
SessionSelection::Resume(target.clone()),
SessionSelection::Fork(target.clone()),
] {
let (mut server, requests, proxy) = start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
)
.await?;
let bootstrap = server.bootstrap(&app.config).await?;
requests.lock().unwrap().clear();
let mut tui = crate::tui::test_support::make_test_tui()?;
let result = super::startup_defaults_tests::run_startup_for_test(
&mut tui,
server,
app.config.clone(),
bootstrap,
selection,
app.app_server_target.clone(),
)
.await;
assert!(
result
.unwrap_err()
.to_string()
.contains("different backend")
);
assert!(requests.lock().unwrap().is_empty());
proxy.abort();
}
Ok(())
}
@@ -716,6 +716,7 @@ async fn command_center_new_preserves_only_selected_server_profiles() -> Result<
)
.await?;
let mut server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Remote,
);
@@ -35,6 +35,7 @@ async fn lost_mutation_reply_preserves_work_without_resubmitting() -> Result<()>
.await
});
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
@@ -429,6 +430,7 @@ async fn lost_initial_thread_reply_keeps_startup_draft_offline() -> Result<()> {
.await
});
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
@@ -103,6 +103,7 @@ async fn history_fixture(
.join("\n");
std::fs::write(&path, format!("{contents}\n"))?;
let target = SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(path),
thread_id,
cwd: None,
@@ -292,6 +293,7 @@ async fn history_hydration_archived_retry_uses_first_attempt_runtime_settings()
}
};
assert!(initial.is_err());
target.backend_id = server.backend_id();
let outcome = complete_session_start(
&mut server,
SessionStartConfig {
@@ -211,6 +211,7 @@ async fn model_default_saves_report_server_outcomes_and_target_server_profile()
)
.await?;
let mut server = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Remote,
);
@@ -251,6 +251,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
Ok::<_, color_eyre::Report>(methods)
});
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Embedded,
);
@@ -313,6 +314,7 @@ async fn reconnect_daemon_command_center_after_socket_replacement_without_a_conv
);
}
let connected = reconnect(
session.backend_id(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
@@ -43,6 +43,7 @@ async fn permission_shortcut_rejections_leave_state_unchanged() -> Result<()> {
)
.await?;
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Embedded,
);
@@ -110,10 +110,12 @@ sandbox = "unelevated"
.await?;
tui.pause_events();
app.launch_cwd = source.path().to_path_buf();
let backend_id = server.backend_id();
app.resume_target_session(
&mut tui,
&mut server,
SessionTarget {
backend_id,
path: None,
thread_id: saved.session.thread_id,
cwd: Some(source.path().to_path_buf()),
@@ -2050,6 +2050,7 @@ async fn overridden_voice_save_keeps_effective_voice() -> Result<()> {
)
.await?;
let server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Embedded,
);
@@ -117,6 +117,7 @@ async fn failed_close_status_read_reconnects_remote_or_closes_embedded() -> Resu
.await
});
let session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
@@ -243,6 +243,7 @@ async fn security_setup_skips_fetch_when_server_auth_does_not_match_saved_login(
.await
});
let server = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Embedded,
);
@@ -322,6 +322,7 @@ async fn same_thread_retry_keeps_subscription_and_restores_draft() -> Result<()>
/*failed_thread_name*/ None,
)
.await?;
let backend_id = app_server.backend_id();
app.enqueue_primary_thread_session(
test_thread_session(thread_id, test_path_buf("/tmp/project")),
Vec::new(),
@@ -343,6 +344,7 @@ async fn same_thread_retry_keeps_subscription_and_restores_draft() -> Result<()>
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path,
thread_id,
cwd: None,
@@ -893,8 +895,12 @@ pub(super) async fn start_recording_app_server_with_realtime_speech(
.await?;
Ok((
AppServerSession::new(app_server, thread_params_mode)
.with_local_codex_home(&config.codex_home),
AppServerSession::new(
crate::backend_id::BackendId::new(),
app_server,
thread_params_mode,
)
.with_local_codex_home(&config.codex_home),
requests,
proxy,
))
@@ -4814,6 +4820,7 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
Some("Failed Fork"),
)
.await?;
let backend_id = app_server.backend_id();
let root = app_server
.resume_thread(
&app.local_settings,
@@ -4930,6 +4937,7 @@ fn session_lifecycle_avoids_redundant_subagent_metadata_reads() -> Result<()> {
&mut tui,
&mut app_server,
crate::resume_picker::SessionTarget {
backend_id,
path: Some(root_rollout_path),
thread_id: root_thread_id,
cwd: None,
@@ -5372,3 +5380,6 @@ async fn command_center_read_only_open_requests_and_failure_preservation() -> Re
}
Ok(())
}
#[path = "backend_ownership_tests.rs"]
mod backend_ownership_tests;
+11
View File
@@ -268,6 +268,7 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
assert_eq!(
App::should_wait_for_initial_session(&SessionSelection::Resume(
crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/restore")),
thread_id: ThreadId::new(),
cwd: None,
@@ -279,6 +280,7 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
assert_eq!(
App::should_wait_for_initial_session(&SessionSelection::Fork(
crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/fork")),
thread_id: ThreadId::new(),
cwd: None,
@@ -292,12 +294,14 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
#[test]
fn startup_paused_goal_prompt_gate_is_only_for_quiet_resume() {
let resume = SessionSelection::Resume(crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/restore")),
thread_id: ThreadId::new(),
cwd: None,
history_mode: None,
});
let fork = SessionSelection::Fork(crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/fork")),
thread_id: ThreadId::new(),
cwd: None,
@@ -368,6 +372,7 @@ fn startup_waiting_gate_holds_active_thread_events_until_primary_thread_configur
fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
let wait_for_resume = App::should_wait_for_initial_session(&SessionSelection::Resume(
crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/restore")),
thread_id: ThreadId::new(),
cwd: None,
@@ -383,6 +388,7 @@ fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
);
let wait_for_fork = App::should_wait_for_initial_session(&SessionSelection::Fork(
crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(PathBuf::from("/tmp/fork")),
thread_id: ThreadId::new(),
cwd: None,
@@ -940,6 +946,7 @@ async fn unattended_modals_survive_unfinished_terminal_input() -> Result<()> {
app.render_startup_frame(&mut tui, &app_event_rx)?;
assert!(app.begin_reconnect());
let connected = super::super::reconnect::reconnect(
app_server.backend_id(),
app.app_server_target.clone(),
app.config.clone(),
app.local_settings.clone(),
@@ -1416,6 +1423,7 @@ async fn remote_overview_startup_hides_disabled_older_server_notice() -> Result<
.await
});
let session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
@@ -1796,6 +1804,7 @@ async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
while app_event_rx.try_recv().is_ok() {}
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(test_path_buf("/tmp/project")),
thread_id,
cwd: None,
@@ -1822,6 +1831,7 @@ async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread(
app.chat_widget.handle_thread_session(session);
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(test_path_buf("/tmp/project")),
thread_id,
cwd: None,
@@ -1849,6 +1859,7 @@ async fn ignore_same_thread_resume_allows_retrying_read_only_view() -> Result<()
assert!(
!app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(test_path_buf("/tmp/project")),
thread_id,
cwd: None,
@@ -15,7 +15,7 @@ fn remote_target() -> AppServerTarget {
}
}
async fn run_startup_for_test(
pub(super) async fn run_startup_for_test(
tui: &mut crate::tui::Tui,
server: AppServerSession,
config: Config,
@@ -139,6 +139,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
.expect("create source rollout"),
)?;
let (mut server, requests, proxy) = start_recording_remote_app_server(&config).await?;
let backend_id = server.backend_id();
let mut bootstrap = server.bootstrap(&config).await?;
let mut target_preset = bootstrap
.available_models
@@ -182,6 +183,7 @@ async fn cli_fork_omits_implicit_model_and_effort() -> Result<()> {
config,
bootstrap,
SessionSelection::Fork(crate::resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: source,
cwd: None,
@@ -106,6 +106,7 @@ async fn owned_startup_preserves_loading_until_resume_replay_is_applied() -> Res
let (mut app, mut events, _ops) = make_test_app_with_channels().await;
app.local_settings.tui.show_tooltips = false;
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let backend_id = app_server.backend_id();
let mut tui = crate::tui::test_support::make_test_tui()?;
tui.set_owned_screen(/*owned*/ true)?;
let thread_id = ThreadId::new();
@@ -117,6 +118,7 @@ async fn owned_startup_preserves_loading_until_resume_replay_is_applied() -> Res
startup.update_session_selection(
&mut tui,
&SessionSelection::Resume(crate::resume_picker::SessionTarget {
backend_id,
path: None,
thread_id,
cwd: None,
@@ -216,7 +216,7 @@ async fn run_verification_rpc_scenario(scenario: RpcScenario) -> color_eyre::Res
} else {
crate::app_server_session::ThreadParamsMode::Embedded
};
let mut session = AppServerSession::new(client, mode);
let mut session = AppServerSession::new(crate::backend_id::BackendId::new(), client, mode);
let buffered = crate::app::app_server_requests::BufferedServerRequest::new(
request.clone(),
&session.request_handle(),
+11 -1
View File
@@ -322,6 +322,7 @@ pub(crate) struct AppServerBootstrap {
}
pub(crate) struct AppServerSession {
backend_id: crate::backend_id::BackendId,
client: AppServerClient,
next_request_id: i64,
history_pagination: HashMap<ThreadId, history::ThreadHistoryPagination>,
@@ -428,8 +429,13 @@ impl AppServerSession {
self.client.platform_os()
}
pub(crate) fn new(client: AppServerClient, thread_params_mode: ThreadParamsMode) -> Self {
pub(crate) fn new(
backend_id: crate::backend_id::BackendId,
client: AppServerClient,
thread_params_mode: ThreadParamsMode,
) -> Self {
Self {
backend_id,
client,
next_request_id: 1,
history_pagination: HashMap::new(),
@@ -451,6 +457,10 @@ impl AppServerSession {
}
}
pub(crate) fn backend_id(&self) -> crate::backend_id::BackendId {
self.backend_id
}
pub(crate) async fn start_dynamic_tool_mcp(
&mut self,
config: Config,
@@ -66,6 +66,7 @@ async fn collaboration_catalog_is_optional_and_refetched_on_bootstrap() -> Resul
Ok::<_, color_eyre::Report>(catalog_requests)
});
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
);
@@ -58,8 +58,11 @@ async fn external_agent_config_migration_over_shared_transport() -> Result<()> {
}
Ok::<_, color_eyre::Report>(requests)
});
let mut session =
AppServerSession::new(crate::connect_remote_app_server(endpoint).await?, mode);
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
mode,
);
let workspace = tempfile::tempdir()?;
let cwd = workspace.path();
let mut tui = crate::tui::test_support::make_test_tui()?;
@@ -58,6 +58,7 @@ async fn thread_list_preserves_filters_and_retries_only_legacy_cwd_errors() -> R
expected.push(fallback);
}
let mut session = AppServerSession::new(
crate::backend_id::BackendId::new(),
crate::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Embedded,
);
+12
View File
@@ -0,0 +1,12 @@
//! Logical backend identity, independent of a transport connection or native thread ID.
//! Reconnecting and auxiliary picker connections reuse the launch owner; a different
//! backend gets a fresh identity. These process-local IDs never appear on the wire.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct BackendId(uuid::Uuid);
impl BackendId {
pub(crate) fn new() -> Self {
Self(uuid::Uuid::new_v4())
}
}
+5 -1
View File
@@ -364,7 +364,11 @@ async fn daemon_startup_falls_back_only_for_implicit_endpoints() -> color_eyre::
assert_eq!(target, original_target);
assert!(state_db.is_none());
} else {
let server = AppServerSession::new(result?, target.thread_params_mode());
let server = AppServerSession::new(
crate::backend_id::BackendId::new(),
result?,
target.thread_params_mode(),
);
assert!(server.uses_embedded_app_server());
assert_eq!(target, AppServerTarget::Embedded);
assert!(state_db.is_some());
+96 -21
View File
@@ -119,6 +119,7 @@ mod app_server_session;
mod approval_events;
mod async_question_reply;
mod backend_banners;
mod backend_id;
mod bottom_pane;
mod branch_summary;
mod chatwidget;
@@ -340,6 +341,19 @@ pub(crate) enum AppServerTarget {
}
impl AppServerTarget {
fn preserve_selected_backend(&mut self, selection: &resume_picker::SessionSelection) {
if matches!(
selection,
resume_picker::SessionSelection::Resume(_) | resume_picker::SessionSelection::Fork(_)
) && let Self::LocalDaemon {
allow_embedded_fallback,
..
} = self
{
*allow_embedded_fallback = false;
}
}
pub(crate) fn uses_remote_workspace(&self) -> bool {
matches!(self, Self::Remote { .. })
}
@@ -622,6 +636,7 @@ async fn start_app_server(
}
pub(crate) async fn start_app_server_for_picker(
backend_id: crate::backend_id::BackendId,
config: &Config,
target: &AppServerTarget,
cli_kv_overrides: Vec<(String, toml::Value)>,
@@ -630,6 +645,14 @@ pub(crate) async fn start_app_server_for_picker(
environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerSession> {
let mut target = target.clone();
// The picker must connect to the backend that owns the supplied ID.
if let AppServerTarget::LocalDaemon {
allow_embedded_fallback,
..
} = &mut target
{
*allow_embedded_fallback = false;
}
let mut state_db = state_db;
let embedded_network_policy =
codex_app_server_client::EmbeddedNetworkPolicy::load(&loader_overrides).await;
@@ -649,7 +672,7 @@ pub(crate) async fn start_app_server_for_picker(
)
.await?;
Ok(
AppServerSession::new(app_server, target.thread_params_mode())
AppServerSession::new(backend_id, app_server, target.thread_params_mode())
.with_local_codex_home(&config.codex_home),
)
}
@@ -675,10 +698,12 @@ pub(crate) async fn start_embedded_app_server_for_picker(
Default::default(),
)
.await?;
Ok(
AppServerSession::new(app_server, target.thread_params_mode())
.with_local_codex_home(&config.codex_home),
Ok(AppServerSession::new(
crate::backend_id::BackendId::new(),
app_server,
target.thread_params_mode(),
)
.with_local_codex_home(&config.codex_home))
}
#[allow(clippy::too_many_arguments)]
@@ -757,10 +782,12 @@ async fn shutdown_startup_session(
}
fn session_target_from_app_server_thread(
backend_id: crate::backend_id::BackendId,
thread: AppServerThread,
) -> Option<resume_picker::SessionTarget> {
match ThreadId::from_string(&thread.id) {
Ok(thread_id) => Some(resume_picker::SessionTarget {
backend_id,
path: thread.path,
thread_id,
cwd: Some(thread.cwd.to_path_buf()),
@@ -809,7 +836,10 @@ async fn lookup_session_target_with_app_server(
.thread_read(thread_id, /*include_turns*/ false)
.await
{
Ok(thread) => Ok(session_target_from_app_server_thread(thread)),
Ok(thread) => Ok(session_target_from_app_server_thread(
app_server.backend_id(),
thread,
)),
Err(err) => {
warn!(
session = id_or_name,
@@ -831,7 +861,7 @@ async fn lookup_session_target_with_app_server(
model_provider.as_deref(),
)
.await?
.and_then(session_target_from_app_server_thread))
.and_then(|thread| session_target_from_app_server_thread(app_server.backend_id(), thread)))
}
async fn lookup_latest_session_target_with_app_server(
@@ -860,10 +890,9 @@ async fn lookup_latest_session_target_with_app_server(
Err(_) if lookup_mode == LatestSessionLookupMode::StateDbOnly => continue,
response => response?,
};
let target = response
.data
.into_iter()
.find_map(session_target_from_app_server_thread);
let target = response.data.into_iter().find_map(|thread| {
session_target_from_app_server_thread(app_server.backend_id(), thread)
});
if let Some(target) = target
&& (uses_remote_workspace
|| match target.path.as_deref() {
@@ -942,7 +971,12 @@ fn uses_remote_workspace_or_environment(
.is_some_and(|environment| environment.is_remote())
}
#[expect(
clippy::too_many_arguments,
reason = "validate selection even after the picker connection is closed"
)]
async fn resolve_startup_resume_or_fork_cwd(
backend_id: crate::backend_id::BackendId,
tui: &mut Tui,
config: &Config,
app_server: Option<&mut AppServerSession>,
@@ -962,6 +996,7 @@ async fn resolve_startup_resume_or_fork_cwd(
}) else {
return Ok(ResolveCwdOutcome::Continue(None));
};
target_session.ensure_backend(backend_id)?;
let local_settings = crate::local_settings::LocalSettings::from(config);
let resume_cwd_mode = effective_resume_cwd_mode(local_settings.tui.resume_cwd, cwd_override);
if uses_remote_workspace_or_environment
@@ -1171,6 +1206,7 @@ async fn run_ratatui_app(
>,
startup_draft: startup_draft::StartupDraft,
) -> color_eyre::Result<AppExitInfo> {
let backend_id = crate::backend_id::BackendId::new();
let uses_remote_workspace = app_server_target.uses_remote_workspace();
let workload_identity_selected = is_workload_identity_selected();
color_eyre::install()?;
@@ -1238,10 +1274,12 @@ async fn run_ratatui_app(
tui.terminal_size_tags =
launch_telemetry.record(&app_server_target, matches!(&startup_app_server, Ok(Ok(_))));
let mut app_server_session = match startup_app_server {
Ok(Ok(app_server)) => {
AppServerSession::new(app_server, app_server_target.thread_params_mode())
.with_local_codex_home(&initial_config.codex_home)
}
Ok(Ok(app_server)) => AppServerSession::new(
backend_id,
app_server,
app_server_target.thread_params_mode(),
)
.with_local_codex_home(&initial_config.codex_home),
Ok(Err(err)) => {
terminal_restore_guard.restore_silently();
session_log::log_session_end();
@@ -1689,6 +1727,7 @@ async fn run_ratatui_app(
let current_cwd = config.cwd.clone();
let fallback_cwd = match resolve_startup_resume_or_fork_cwd(
backend_id,
&mut tui,
&config,
app_server.as_mut(),
@@ -1792,6 +1831,10 @@ async fn run_ratatui_app(
shutdown_app_server_if_present(app_server.take()).await;
}
}
// Pinning a selected backend changes fallback eligibility, not the launch network policy.
let uses_embedded_network_policy = app_server_target.uses_embedded_network_policy();
// A startup picker may consume its connection, but its selection keeps that backend.
app_server_target.preserve_selected_backend(&session_selection);
let mut app_server = match app_server {
Some(app_server) => app_server,
None => match startup_draft
@@ -1817,9 +1860,13 @@ async fn run_ratatui_app(
Ok(Ok(app_server)) => {
// A picker can replace the server; account reads belong to their original session.
startup_account = None;
AppServerSession::new(app_server, app_server_target.thread_params_mode())
.with_local_codex_home(&config.codex_home)
.with_remote_cwd_override(remote_cwd_override.clone())
AppServerSession::new(
backend_id,
app_server,
app_server_target.thread_params_mode(),
)
.with_local_codex_home(&config.codex_home)
.with_remote_cwd_override(remote_cwd_override.clone())
}
Ok(Err(err)) => {
terminal_restore_guard.restore_silently();
@@ -1834,6 +1881,12 @@ async fn run_ratatui_app(
},
};
if let resume_picker::SessionSelection::Resume(target)
| resume_picker::SessionSelection::Fork(target) = &session_selection
{
target.ensure_backend(app_server.backend_id())?;
}
if let resume_picker::SessionSelection::Resume(target) = &session_selection {
let validation = startup_draft
.run_until(
@@ -1901,7 +1954,7 @@ async fn run_ratatui_app(
managed_worktree.as_ref(),
)
.await;
if app_server_target.uses_embedded_network_policy() {
if uses_embedded_network_policy {
embedded_network_policy.bind_config(&mut config);
}
if config.model_provider_id != previous_provider
@@ -1923,8 +1976,12 @@ async fn run_ratatui_app(
embedded_network_policy.clone(),
)
.await?;
app_server = AppServerSession::new(client, app_server_target.thread_params_mode())
.with_local_codex_home(&config.codex_home);
app_server = AppServerSession::new(
backend_id,
client,
app_server_target.thread_params_mode(),
)
.with_local_codex_home(&config.codex_home);
}
#[cfg(target_os = "windows")]
{
@@ -1960,7 +2017,7 @@ async fn run_ratatui_app(
startup_draft.update_session_selection(&mut tui, &session_selection)?;
}
}
if app_server_target.uses_embedded_network_policy() {
if uses_embedded_network_policy {
embedded_network_policy.bind_config(&mut config);
}
startup_draft.apply_settings(
@@ -2475,6 +2532,7 @@ requires_openai_auth = {requires_openai_auth}
)?;
let server_config = build_config(&home).await?;
let mut server = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::InProcess(start_test_embedded_app_server(server_config).await?),
ThreadParamsMode::Embedded,
);
@@ -2789,6 +2847,7 @@ requires_openai_auth = {requires_openai_auth}
let state_db =
init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await?;
let mut app_server = start_app_server_for_picker(
crate::backend_id::BackendId::new(),
&config,
&AppServerTarget::Embedded,
Vec::new(),
@@ -2810,6 +2869,7 @@ requires_openai_auth = {requires_openai_auth}
#[tokio::test]
async fn startup_resume_and_fork_use_configured_or_explicit_cwd() -> color_eyre::Result<()> {
let backend_id = crate::backend_id::BackendId::new();
for (action, configured_mode, has_explicit_cwd, expected_directory) in [
(CwdPromptAction::Resume, "current", false, "launch"),
(CwdPromptAction::Resume, "session", false, "session"),
@@ -2856,6 +2916,7 @@ requires_openai_auth = {requires_openai_auth}
let state_db =
init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await?;
let target_session = resume_picker::SessionTarget {
backend_id,
path: Some(rollout_path),
thread_id,
cwd: Some(session_cwd.clone()),
@@ -2868,6 +2929,7 @@ requires_openai_auth = {requires_openai_auth}
let mut tui = tui::test_support::make_test_tui()?;
let fallback_cwd = match resolve_startup_resume_or_fork_cwd(
backend_id,
&mut tui,
&config,
/*app_server*/ None,
@@ -2900,6 +2962,7 @@ requires_openai_auth = {requires_openai_auth}
&expected_cwd,
));
let mut app_server = start_app_server_for_picker(
crate::backend_id::BackendId::new(),
&final_config,
&AppServerTarget::Embedded,
Vec::new(),
@@ -2942,6 +3005,7 @@ requires_openai_auth = {requires_openai_auth}
#[tokio::test]
async fn startup_remote_current_cwd_without_override_is_rejected() -> color_eyre::Result<()> {
let backend_id = crate::backend_id::BackendId::new();
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("config.toml"),
@@ -2955,10 +3019,12 @@ requires_openai_auth = {requires_openai_auth}
let mut tui = tui::test_support::make_test_tui()?;
let error = resolve_startup_resume_or_fork_cwd(
backend_id,
&mut tui,
&config,
/*app_server*/ None,
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -2976,10 +3042,12 @@ requires_openai_auth = {requires_openai_auth}
"`tui.resume_cwd = \"current\"` requires `--cd` when using a remote workspace"
);
let explicit = resolve_startup_resume_or_fork_cwd(
backend_id,
&mut tui,
&config,
/*app_server*/ None,
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -2998,6 +3066,7 @@ requires_openai_auth = {requires_openai_auth}
#[tokio::test]
async fn startup_session_cwd_without_metadata_is_rejected() -> color_eyre::Result<()> {
let backend_id = crate::backend_id::BackendId::new();
let temp_dir = TempDir::new()?;
std::fs::write(
temp_dir.path().join("config.toml"),
@@ -3011,10 +3080,12 @@ requires_openai_auth = {requires_openai_auth}
let mut tui = tui::test_support::make_test_tui()?;
let error = resolve_startup_resume_or_fork_cwd(
backend_id,
&mut tui,
&config,
/*app_server*/ None,
&resume_picker::SessionSelection::Resume(resume_picker::SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -3063,6 +3134,7 @@ requires_openai_auth = {requires_openai_auth}
fn session_target_display_label_falls_back_to_thread_id() {
let thread_id = ThreadId::new();
let target = crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: None,
thread_id,
cwd: None,
@@ -3545,6 +3617,7 @@ requires_openai_auth = {requires_openai_auth}
});
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config.clone()).await?,
),
@@ -3608,6 +3681,7 @@ requires_openai_auth = {requires_openai_auth}
.build()
.await?;
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config.clone()).await?,
),
@@ -3819,6 +3893,7 @@ requires_openai_auth = {requires_openai_auth}
config.terminal_resize_reflow.max_rows =
crate::legacy_core::config::TerminalResizeReflowMaxRows::Limit(2);
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::InProcess(start_test_embedded_app_server(config.clone()).await?),
ThreadParamsMode::Embedded,
);
@@ -57,6 +57,7 @@ async fn lookup_name(
model_provider: Option<&str>,
) -> color_eyre::Result<Option<codex_app_server_protocol::Thread>> {
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config.clone()).await?,
),
@@ -304,6 +305,7 @@ async fn rejects_duplicate_labels_across_server_pages() -> color_eyre::Result<()
}
let mut app_server = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config.clone()).await?,
),
@@ -433,6 +435,7 @@ async fn uses_listed_thread_when_older_server_cannot_read_it() -> color_eyre::Re
)
.await?;
let mut embedded = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config.clone()).await?,
),
@@ -527,6 +530,7 @@ async fn uses_listed_thread_when_older_server_cannot_read_it() -> color_eyre::Re
})
.await?;
let mut remote = AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::Remote(client),
mode,
);
@@ -114,7 +114,7 @@ async fn discovery_preserves_config_scope_and_bounds_server_requests() {
})
.await
.unwrap();
let session = AppServerSession::new(client, mode)
let session = AppServerSession::new(crate::backend_id::BackendId::new(), client, mode)
.with_remote_cwd_override(remote_cwd.map(std::path::PathBuf::from));
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
if case == "timeout" {
+92
View File
@@ -109,6 +109,8 @@ const PICKER_LIST_HORIZONTAL_INSET: u16 = 4;
#[derive(Debug, Clone)]
pub struct SessionTarget {
/// Logical owner of the native thread ID, retained across connection replacement.
pub(crate) backend_id: crate::backend_id::BackendId,
pub path: Option<PathBuf>,
pub thread_id: ThreadId,
/// Working directory reported by `thread/list` or `thread/read` at selection time.
@@ -118,6 +120,14 @@ pub struct SessionTarget {
}
impl SessionTarget {
pub(crate) fn ensure_backend(&self, backend_id: crate::backend_id::BackendId) -> Result<()> {
color_eyre::eyre::ensure!(
self.backend_id == backend_id,
"This session belongs to a different backend. Open the session picker again."
);
Ok(())
}
pub fn display_label(&self) -> String {
self.path
.as_ref()
@@ -338,6 +348,7 @@ struct SessionPickerViewPersistence {
}
struct SessionPickerRunOptions {
backend_id: crate::backend_id::BackendId,
use_theme_colors: bool,
copy_on_select: bool,
copy_on_select_clear_selection: bool,
@@ -450,6 +461,7 @@ async fn run_resume_picker_with_launch_context(
let provider_filter = picker_provider_filter(config, &app_server).await?;
let runtime_keymap = picker_runtime_keymap(local_settings)?;
let options = SessionPickerRunOptions {
backend_id: app_server.backend_id(),
use_theme_colors: local_settings.tui.status_line_use_colors,
copy_on_select: local_settings.copy_on_select(&codex_terminal_detection::terminal_info()),
copy_on_select_clear_selection: local_settings.tui.copy_on_select_clear_selection,
@@ -511,6 +523,7 @@ pub async fn run_fork_picker_with_app_server(
let provider_filter = picker_provider_filter(config, &app_server).await?;
let runtime_keymap = picker_runtime_keymap(local_settings)?;
let options = SessionPickerRunOptions {
backend_id: app_server.backend_id(),
use_theme_colors: local_settings.tui.status_line_use_colors,
copy_on_select: local_settings.copy_on_select(&codex_terminal_detection::terminal_info()),
copy_on_select_clear_selection: local_settings.tui.copy_on_select_clear_selection,
@@ -559,6 +572,7 @@ async fn run_session_picker_with_loader(
) -> Result<SessionSelection> {
let alt = AltScreenGuard::enter(tui);
let mut state = PickerState::new(
options.backend_id,
alt.tui.frame_requester(),
picker_loader,
options.provider_filter,
@@ -793,6 +807,7 @@ fn spawn_app_server_page_loader(
})
.await
.map(|response| SessionTarget {
backend_id: app_server.backend_id(),
path: response.thread.path,
thread_id,
cwd: Some(response.thread.cwd.to_path_buf()),
@@ -842,6 +857,7 @@ impl Drop for AltScreenGuard<'_> {
}
struct PickerState {
backend_id: crate::backend_id::BackendId,
clock_format: ClockFormat,
use_theme_colors: bool,
copy_on_select: bool,
@@ -1036,6 +1052,7 @@ impl Row {
impl PickerState {
fn new(
backend_id: crate::backend_id::BackendId,
requester: FrameRequester,
picker_loader: PickerLoader,
provider_filter: ProviderFilter,
@@ -1044,6 +1061,7 @@ impl PickerState {
action: SessionPickerAction,
) -> Self {
Self {
backend_id,
clock_format: ClockFormat::system(),
use_theme_colors: true,
copy_on_select: false,
@@ -1321,6 +1339,7 @@ impl PickerState {
return Ok(None);
}
return Ok(Some(self.action.selection(SessionTarget {
backend_id: self.backend_id,
path,
thread_id,
cwd: row.cwd.clone(),
@@ -3677,6 +3696,7 @@ mod tests {
request_sink.lock().unwrap().push(request);
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -3771,6 +3791,7 @@ mod tests {
let primary = dunce::canonicalize(primary).unwrap();
let linked = dunce::canonicalize(linked).unwrap();
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
page_only_loader(|_| {}),
ProviderFilter::MatchDefault("openai".to_string()),
@@ -3848,6 +3869,7 @@ mod tests {
bg_tx,
);
let mut live_picker = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(config.model_provider_id.clone()),
@@ -4160,6 +4182,7 @@ mod tests {
ThreadId::from_string("019dabc1-0ef5-7431-b81c-03037f51f62c").expect("thread id");
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4324,6 +4347,7 @@ mod tests {
);
row.cwd = Some(cwd.clone());
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
page_only_loader(|_| {}),
ProviderFilter::MatchDefault("server-provider".to_string()),
@@ -4396,6 +4420,7 @@ mod tests {
});
let remote_cwd = Some(PathBuf::from("/srv/link-project"));
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::Any,
@@ -4433,6 +4458,7 @@ mod tests {
fn remote_picker_does_not_filter_rows_by_local_cwd() {
let loader = page_only_loader(|_| {});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::Any,
@@ -4458,6 +4484,7 @@ mod tests {
fn resume_table_snapshot() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4517,6 +4544,7 @@ mod tests {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4549,6 +4577,7 @@ mod tests {
fn hint_line_switches_esc_label_for_search_mode() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4568,6 +4597,7 @@ mod tests {
fn hint_line_labels_cancel_keys_as_exit_for_existing_session_resume_picker() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4596,6 +4626,7 @@ mod tests {
fn hint_line_switches_density_label() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4627,6 +4658,7 @@ mod tests {
fn hint_line_compacts_on_narrow_width() {
let loader = page_only_loader(|_| {});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4650,6 +4682,7 @@ mod tests {
fn hint_line_snapshot_uses_distributed_wide_footer() {
let loader = page_only_loader(|_| {});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4668,6 +4701,7 @@ mod tests {
fn hint_line_snapshot_uses_compact_footer() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4688,6 +4722,7 @@ mod tests {
fn hint_line_preserves_primary_action_labels_when_very_narrow() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4716,6 +4751,7 @@ mod tests {
fn hint_line_shows_loading_transcript_mode() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4736,6 +4772,7 @@ mod tests {
fn picker_footer_percent_reports_scroll_progress() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4764,6 +4801,7 @@ mod tests {
fn picker_footer_progress_label_shows_position_total_and_percent() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4792,6 +4830,7 @@ mod tests {
fn picker_footer_progress_label_uses_known_count_when_more_pages_exist() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4820,6 +4859,7 @@ mod tests {
fn picker_footer_progress_label_freezes_percent_while_loading() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4855,6 +4895,7 @@ mod tests {
fn picker_footer_percent_is_complete_when_not_scrollable() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4877,6 +4918,7 @@ mod tests {
async fn ctrl_o_toggles_density_without_typing_into_search() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4906,6 +4948,7 @@ mod tests {
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -4943,6 +4986,7 @@ mod tests {
let loader = page_only_loader(|_| {});
let thread_id = ThreadId::new();
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5003,6 +5047,7 @@ mod tests {
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5051,6 +5096,7 @@ mod tests {
async fn transcript_loading_still_allows_ctrl_c_exit() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5075,6 +5121,7 @@ mod tests {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5143,6 +5190,7 @@ mod tests {
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5173,6 +5221,7 @@ mod tests {
async fn ctrl_t_on_row_without_thread_id_shows_inline_error() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5209,6 +5258,7 @@ mod tests {
let thread_id = ThreadId::new();
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5314,6 +5364,7 @@ mod tests {
let thread_id = ThreadId::new();
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5358,6 +5409,7 @@ mod tests {
let tmp = tempdir().expect("tmpdir");
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5392,6 +5444,7 @@ session_picker_view = "dense"
std::fs::write(&codex_home_file, "not a directory").expect("write codex home file");
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5423,6 +5476,7 @@ session_picker_view = "dense"
async fn raw_ctrl_o_toggles_density_without_typing_into_search() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5445,6 +5499,7 @@ session_picker_view = "dense"
async fn space_appends_to_search_query() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5478,6 +5533,7 @@ session_picker_view = "dense"
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5517,6 +5573,7 @@ session_picker_view = "dense"
let thread_id = ThreadId::new();
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5550,6 +5607,7 @@ session_picker_view = "dense"
let loader = page_only_loader(|_| {});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5580,6 +5638,7 @@ session_picker_view = "dense"
fn toolbar_compacts_on_narrow_width() {
let loader = page_only_loader(|_| {});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5626,6 +5685,7 @@ session_picker_view = "dense"
let loader = page_only_loader(|_| {});
let row = dense_snapshot_row();
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5685,6 +5745,7 @@ session_picker_view = "dense"
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5747,6 +5808,7 @@ session_picker_view = "dense"
fn comfortable_zebra_lines_use_full_width_background() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5788,6 +5850,7 @@ session_picker_view = "dense"
second.preview = String::from("Second dense row");
second.git_branch = Some(String::from("fcoury/other-branch"));
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5840,6 +5903,7 @@ session_picker_view = "dense"
git_branch: Some(String::from("fcoury/session-picker")),
};
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5912,6 +5976,7 @@ session_picker_view = "dense"
git_branch: Some(String::from("fcoury/session-picker")),
};
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -5950,6 +6015,7 @@ session_picker_view = "dense"
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6002,6 +6068,7 @@ session_picker_view = "dense"
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6057,6 +6124,7 @@ session_picker_view = "dense"
fn pageless_scrolling_deduplicates_and_keeps_order() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6117,6 +6185,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6151,6 +6220,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6186,6 +6256,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6228,6 +6299,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6270,6 +6342,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6304,6 +6377,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6338,6 +6412,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::Any,
@@ -6380,6 +6455,7 @@ session_picker_view = "dense"
async fn page_navigation_uses_view_rows() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6439,6 +6515,7 @@ session_picker_view = "dense"
async fn page_and_jump_navigation_use_list_keymap() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6501,6 +6578,7 @@ session_picker_view = "dense"
async fn ctrl_c_exits_even_when_cancel_is_remapped_to_ctrl_c() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6526,6 +6604,7 @@ session_picker_view = "dense"
request_sink.lock().unwrap().push(req);
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6570,6 +6649,7 @@ session_picker_view = "dense"
async fn enter_on_row_without_resolvable_thread_id_shows_inline_error() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6609,6 +6689,7 @@ session_picker_view = "dense"
async fn enter_on_pathless_thread_uses_thread_id() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6637,6 +6718,7 @@ session_picker_view = "dense"
match selection {
Some(SessionSelection::Resume(SessionTarget {
backend_id: _,
path: None,
thread_id: selected_thread_id,
cwd: None,
@@ -6930,6 +7012,7 @@ session_picker_view = "dense"
async fn moving_to_last_card_scrolls_when_cards_exceed_viewport() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -6972,6 +7055,7 @@ session_picker_view = "dense"
async fn up_from_bottom_keeps_viewport_stable_when_card_remains_visible() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7014,6 +7098,7 @@ session_picker_view = "dense"
async fn up_scrolls_only_after_crossing_top_edge() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7052,6 +7137,7 @@ session_picker_view = "dense"
fn list_reports_more_rows_above_and_below() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7093,6 +7179,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7209,6 +7296,7 @@ session_picker_view = "dense"
async fn paste_appends_to_existing_query() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7227,6 +7315,7 @@ session_picker_view = "dense"
async fn whitespace_only_paste_is_ignored() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7250,6 +7339,7 @@ session_picker_view = "dense"
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7282,6 +7372,7 @@ session_picker_view = "dense"
async fn esc_with_empty_query_starts_fresh() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -7302,6 +7393,7 @@ session_picker_view = "dense"
async fn esc_with_query_clears_search_and_preserves_selected_result() {
let loader = page_only_loader(|_| {});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::MatchDefault(String::from("openai")),
@@ -37,6 +37,7 @@ fn archive_picker_state() -> (PickerState, Arc<Mutex<Vec<ThreadId>>>) {
}
});
let state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
picker_loader,
ProviderFilter::Any,
@@ -176,6 +177,7 @@ fn archived_status_preserves_directory_filter_and_hides_archive_shortcut() {
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
picker_loader,
ProviderFilter::Any,
@@ -256,6 +258,7 @@ async fn archived_session_restore_resumes_after_completion() {
let selection = state.handle_unarchive_result(
thread_id,
Ok(SessionTarget {
backend_id: state.backend_id,
path: None,
thread_id,
cwd: None,
@@ -266,6 +269,7 @@ async fn archived_session_restore_resumes_after_completion() {
assert!(matches!(
selection,
Some(SessionSelection::Resume(SessionTarget {
backend_id: _,
path: None,
thread_id: resumed_thread_id,
cwd: None,
@@ -25,6 +25,7 @@ fn thread_colors_and_selection_contrast_in_both_picker_layouts() {
git_branch: None,
};
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
Arc::new(|_| {}),
ProviderFilter::Any,
@@ -149,6 +150,7 @@ fn compact_picker_keeps_metadata_and_labeled_primary_actions() {
let timestamp = parse_timestamp_str("2026-09-17T12:00:00Z");
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
Arc::new(|_| {}),
ProviderFilter::Any,
@@ -258,6 +260,7 @@ async fn narrow_toolbar_keeps_keyboard_focused_control_visible() {
for action in [SessionPickerAction::Resume, SessionPickerAction::Fork] {
for (width, height) in [(38, 12), (24, 9)] {
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
Arc::new(|_| {}),
ProviderFilter::Any,
@@ -17,6 +17,7 @@ async fn later_page_error_preserves_loaded_sessions() {
}
});
let mut state = PickerState::new(
crate::backend_id::BackendId::new(),
FrameRequester::test_dummy(),
loader,
ProviderFilter::Any,
+6 -3
View File
@@ -263,6 +263,7 @@ pub(super) async fn start_app_server_for_session_command(
.await
.wrap_err("failed to load config.toml")?;
return Ok(AppServerSession::new(
crate::backend_id::BackendId::new(),
super::connect_remote_app_server(endpoint).await?,
ThreadParamsMode::Remote,
)
@@ -405,10 +406,12 @@ pub(super) async fn start_app_server_for_session_command(
embedded_network_policy,
)
.await?;
Ok(
AppServerSession::new(app_server, app_server_target.thread_params_mode())
.with_remote_cwd_override(remote_cwd_override),
Ok(AppServerSession::new(
crate::backend_id::BackendId::new(),
app_server,
app_server_target.thread_params_mode(),
)
.with_remote_cwd_override(remote_cwd_override))
}
#[cfg(test)]
@@ -45,6 +45,7 @@ async fn state_runtime(config: &Config) -> std::io::Result<Arc<codex_state::Stat
async fn start_app_server(config: Config) -> color_eyre::Result<AppServerSession> {
Ok(AppServerSession::new(
crate::backend_id::BackendId::new(),
codex_app_server_client::AppServerClient::InProcess(
start_test_embedded_app_server(config).await?,
),
+2
View File
@@ -38,6 +38,7 @@ impl SessionStartAction {
local_settings: &LocalSettings,
target: &SessionTarget,
) -> Result<AppServerStartedThread> {
target.ensure_backend(app_server.backend_id())?;
match self {
Self::Resume(settings, permissions) => {
app_server
@@ -87,6 +88,7 @@ pub(crate) async fn complete_session_start(
initial_result: Result<AppServerStartedThread>,
confirm: impl AsyncFnOnce() -> Result<UnarchiveChoice>,
) -> Result<SessionStartOutcome> {
target.ensure_backend(app_server.backend_id())?;
match initial_result {
Ok(started) => return Ok(SessionStartOutcome::Started(Box::new(started))),
Err(err) => {
+5 -1
View File
@@ -64,13 +64,15 @@ async fn archived_session_requires_confirmation_before_resume_or_fork() -> Resul
std::fs::create_dir_all(&archived_dir)?;
let archived_path = archived_dir.join(active_path.file_name().unwrap());
std::fs::rename(&active_path, &archived_path)?;
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
let backend_id = app_server.backend_id();
let target = SessionTarget {
backend_id,
path: Some(archived_path.clone()),
thread_id: ThreadId::from_string(&id)?,
cwd: None,
history_mode: None,
};
let mut app_server = crate::start_embedded_app_server_for_picker(&config).await?;
let prompts = Cell::new(0);
let local_settings = LocalSettings::from(&config);
@@ -169,6 +171,7 @@ async fn archived_session_requires_confirmation_before_resume_or_fork() -> Resul
assert_eq!(resumed.session.thread_id, target.thread_id);
let missing = SessionTarget {
backend_id,
path: None,
thread_id: ThreadId::new(),
cwd: None,
@@ -194,6 +197,7 @@ fn session_start_error_surfaces_archived_guidance_without_rollout_path() {
let thread_id =
ThreadId::from_string("019e72f4-e09a-70f2-b2c2-a153a57b8cc0").expect("thread id");
let target_session = SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: Some(std::path::PathBuf::from(
"/Users/me/.codex/archived_sessions/rollout.jsonl",
)),
@@ -140,6 +140,7 @@ async fn startup_submission_thread_change_preserves_only_editable_text() {
codex_protocol::ThreadId::new(),
] {
let selection = SessionSelection::Resume(crate::resume_picker::SessionTarget {
backend_id: crate::backend_id::BackendId::new(),
path: None,
thread_id: destination,
cwd: None,
+1
View File
@@ -383,6 +383,7 @@ async fn status_snapshot_shows_chatgpt_plan_without_email() {
.await
.expect("write models cache");
let mut app_server = crate::start_app_server_for_picker(
crate::backend_id::BackendId::new(),
&config,
&crate::AppServerTarget::Embedded,
Vec::new(),
+1
View File
@@ -168,6 +168,7 @@ pub(super) async fn prepare(
)
.await?;
let mut lookup = AppServerSession::new(
crate::backend_id::BackendId::new(),
AppServerClient::InProcess(client),
crate::app_server_session::ThreadParamsMode::Embedded,
);