mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
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:
committed by
copyberry
parent
a083a1e49e
commit
440312f113
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?,
|
||||
),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user