mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add remote --cd forwarding for app-server sessions (#16700)
Addresses #16124 Problem: `codex --remote --cd <path>` canonicalized the path locally and then omitted it from remote thread lifecycle requests, so remote-only working directories failed or were ignored. Solution: Keep remote startup on the local cwd, forward explicit `--cd` values verbatim to `thread/start`, `thread/resume`, and `thread/fork`, and cover the behavior with `codex-tui` tests. Testing: I manually tested `--remote --cd` with both absolute and relative paths and validated correct behavior. --- Update based on code review feedback: Problem: Remote `--cd` was forwarded to `thread/resume` and `thread/fork`, but not to `thread/list` lookups, so `--resume --last` and picker flows could select a session from the wrong cwd; relative cwd filters also failed against stored absolute paths. Solution: Apply explicit remote `--cd` to `thread/list` lookups for `--last` and picker flows, normalize relative cwd filters on the app-server before exact matching, and document/test the behavior.
This commit is contained in:
committed by
GitHub
Unverified
parent
a71fc47cf8
commit
0ab8eda375
@@ -273,7 +273,7 @@ Experimental API: `thread/start`, `thread/resume`, and `thread/fork` accept `per
|
||||
- `modelProviders` — restrict results to specific providers; unset, null, or an empty array will include all providers.
|
||||
- `sourceKinds` — restrict results to specific sources; omit or pass `[]` for interactive sessions only (`cli`, `vscode`).
|
||||
- `archived` — when `true`, list archived threads only. When `false` or `null`, list non-archived threads (default).
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path.
|
||||
- `cwd` — restrict results to threads whose session cwd exactly matches this path. Relative paths are resolved against the app-server process cwd before matching.
|
||||
- `searchTerm` — restrict results to threads whose extracted title contains this substring (case-sensitive).
|
||||
- Responses include `agentNickname` and `agentRole` for AgentControl-spawned thread sub-agents when available.
|
||||
|
||||
|
||||
@@ -3350,6 +3350,13 @@ impl CodexMessageProcessor {
|
||||
cwd,
|
||||
search_term,
|
||||
} = params;
|
||||
let cwd = match normalize_thread_list_cwd_filter(cwd) {
|
||||
Ok(cwd) => cwd,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let requested_page_size = limit
|
||||
.map(|value| value as usize)
|
||||
@@ -3368,7 +3375,7 @@ impl CodexMessageProcessor {
|
||||
model_providers,
|
||||
source_kinds,
|
||||
archived: archived.unwrap_or(false),
|
||||
cwd: cwd.map(PathBuf::from),
|
||||
cwd,
|
||||
search_term,
|
||||
},
|
||||
)
|
||||
@@ -7707,6 +7714,57 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_thread_list_cwd_filter(
|
||||
cwd: Option<String>,
|
||||
) -> Result<Option<PathBuf>, JSONRPCErrorError> {
|
||||
let Some(cwd) = cwd else {
|
||||
return Ok(None);
|
||||
};
|
||||
AbsolutePathBuf::relative_to_current_dir(cwd.as_str())
|
||||
.map(AbsolutePathBuf::into_path_buf)
|
||||
.map(Some)
|
||||
.map_err(|err| JSONRPCErrorError {
|
||||
code: INVALID_PARAMS_ERROR_CODE,
|
||||
message: format!("invalid thread/list cwd filter `{cwd}`: {err}"),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod thread_list_cwd_filter_tests {
|
||||
use super::normalize_thread_list_cwd_filter;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn normalize_thread_list_cwd_filter_preserves_absolute_paths() {
|
||||
let cwd = if cfg!(windows) {
|
||||
String::from(r"C:\srv\repo-b")
|
||||
} else {
|
||||
String::from("/srv/repo-b")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
normalize_thread_list_cwd_filter(Some(cwd.clone())).expect("cwd filter should parse"),
|
||||
Some(PathBuf::from(cwd))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_thread_list_cwd_filter_resolves_relative_paths_against_server_cwd()
|
||||
-> std::io::Result<()> {
|
||||
let expected = AbsolutePathBuf::relative_to_current_dir("repo-b")?.to_path_buf();
|
||||
|
||||
assert_eq!(
|
||||
normalize_thread_list_cwd_filter(Some(String::from("repo-b")))
|
||||
.expect("cwd filter should parse"),
|
||||
Some(expected)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_thread_listener_command(
|
||||
conversation_id: ThreadId,
|
||||
|
||||
@@ -104,6 +104,7 @@ pub(crate) struct AppServerBootstrap {
|
||||
pub(crate) struct AppServerSession {
|
||||
client: AppServerClient,
|
||||
next_request_id: i64,
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -150,9 +151,19 @@ impl AppServerSession {
|
||||
Self {
|
||||
client,
|
||||
next_request_id: 1,
|
||||
remote_cwd_override: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_remote_cwd_override(mut self, remote_cwd_override: Option<PathBuf>) -> Self {
|
||||
self.remote_cwd_override = remote_cwd_override;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn remote_cwd_override(&self) -> Option<&std::path::Path> {
|
||||
self.remote_cwd_override.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn is_remote(&self) -> bool {
|
||||
matches!(self.client, AppServerClient::Remote(_))
|
||||
}
|
||||
@@ -290,7 +301,11 @@ impl AppServerSession {
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id,
|
||||
params: thread_start_params_from_config(config, self.thread_params_mode()),
|
||||
params: thread_start_params_from_config(
|
||||
config,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/start failed during TUI bootstrap")?;
|
||||
@@ -311,6 +326,7 @@ impl AppServerSession {
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
@@ -332,6 +348,7 @@ impl AppServerSession {
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
self.remote_cwd_override.as_deref(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
@@ -839,11 +856,12 @@ fn sandbox_mode_from_policy(
|
||||
fn thread_start_params_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> ThreadStartParams {
|
||||
ThreadStartParams {
|
||||
model: config.model.clone(),
|
||||
model_provider: thread_params_mode.model_provider_from_config(config),
|
||||
cwd: thread_cwd_from_config(config, thread_params_mode),
|
||||
cwd: thread_cwd_from_config(config, thread_params_mode, remote_cwd_override),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(config),
|
||||
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
|
||||
@@ -858,12 +876,13 @@ fn thread_resume_params_from_config(
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> ThreadResumeParams {
|
||||
ThreadResumeParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
model: config.model.clone(),
|
||||
model_provider: thread_params_mode.model_provider_from_config(&config),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
|
||||
@@ -877,12 +896,13 @@ fn thread_fork_params_from_config(
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> ThreadForkParams {
|
||||
ThreadForkParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
model: config.model.clone(),
|
||||
model_provider: thread_params_mode.model_provider_from_config(&config),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode),
|
||||
cwd: thread_cwd_from_config(&config, thread_params_mode, remote_cwd_override),
|
||||
approval_policy: Some(config.permissions.approval_policy.value().into()),
|
||||
approvals_reviewer: approvals_reviewer_override_from_config(&config),
|
||||
sandbox: sandbox_mode_from_policy(config.permissions.sandbox_policy.get().clone()),
|
||||
@@ -893,10 +913,16 @@ fn thread_fork_params_from_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_cwd_from_config(config: &Config, thread_params_mode: ThreadParamsMode) -> Option<String> {
|
||||
fn thread_cwd_from_config(
|
||||
config: &Config,
|
||||
thread_params_mode: ThreadParamsMode,
|
||||
remote_cwd_override: Option<&std::path::Path>,
|
||||
) -> Option<String> {
|
||||
match thread_params_mode {
|
||||
ThreadParamsMode::Embedded => Some(config.cwd.to_string_lossy().to_string()),
|
||||
ThreadParamsMode::Remote => None,
|
||||
ThreadParamsMode::Remote => {
|
||||
remote_cwd_override.map(|cwd| cwd.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1143,22 +1169,39 @@ mod tests {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
|
||||
let params = thread_start_params_from_config(&config, ThreadParamsMode::Embedded);
|
||||
let params = thread_start_params_from_config(
|
||||
&config,
|
||||
ThreadParamsMode::Embedded,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(params.cwd, Some(config.cwd.to_string_lossy().to_string()));
|
||||
assert_eq!(params.model_provider, Some(config.model_provider_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_lifecycle_params_omit_local_overrides_for_remote_sessions() {
|
||||
async fn thread_lifecycle_params_omit_cwd_without_remote_override_for_remote_sessions() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
|
||||
let start = thread_start_params_from_config(&config, ThreadParamsMode::Remote);
|
||||
let resume =
|
||||
thread_resume_params_from_config(config.clone(), thread_id, ThreadParamsMode::Remote);
|
||||
let fork = thread_fork_params_from_config(config, thread_id, ThreadParamsMode::Remote);
|
||||
let start = thread_start_params_from_config(
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
let resume = thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
let fork = thread_fork_params_from_config(
|
||||
config,
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
/*remote_cwd_override*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(start.cwd, None);
|
||||
assert_eq!(resume.cwd, None);
|
||||
@@ -1168,6 +1211,39 @@ mod tests {
|
||||
assert_eq!(fork.model_provider, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_lifecycle_params_forward_explicit_remote_cwd_override_for_remote_sessions() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let remote_cwd = PathBuf::from("repo/on/server");
|
||||
|
||||
let start = thread_start_params_from_config(
|
||||
&config,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
);
|
||||
let resume = thread_resume_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
);
|
||||
let fork = thread_fork_params_from_config(
|
||||
config,
|
||||
thread_id,
|
||||
ThreadParamsMode::Remote,
|
||||
Some(remote_cwd.as_path()),
|
||||
);
|
||||
|
||||
assert_eq!(start.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(resume.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(fork.cwd.as_deref(), Some("repo/on/server"));
|
||||
assert_eq!(start.model_provider, None);
|
||||
assert_eq!(resume.model_provider, None);
|
||||
assert_eq!(fork.model_provider, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resume_response_restores_turns_from_thread_items() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
@@ -95,6 +95,7 @@ pub struct Cli {
|
||||
pub dangerously_bypass_approvals_and_sandbox: bool,
|
||||
|
||||
/// Tell the agent to use the specified directory as its working root.
|
||||
/// In remote mode, the path is forwarded to the server and resolved there.
|
||||
#[clap(long = "cd", short = 'C', value_name = "DIR")]
|
||||
pub cwd: Option<PathBuf>,
|
||||
|
||||
|
||||
+143
-44
@@ -577,15 +577,42 @@ fn latest_session_lookup_params(
|
||||
source_kinds: (!include_non_interactive)
|
||||
.then_some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
|
||||
archived: Some(false),
|
||||
cwd: if is_remote {
|
||||
None
|
||||
} else {
|
||||
cwd_filter.map(|cwd| cwd.to_string_lossy().to_string())
|
||||
},
|
||||
cwd: cwd_filter.map(|cwd| cwd.to_string_lossy().to_string()),
|
||||
search_term: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_cwd_for_app_server_target(
|
||||
cwd: Option<&Path>,
|
||||
app_server_target: &AppServerTarget,
|
||||
) -> std::io::Result<AbsolutePathBuf> {
|
||||
if matches!(app_server_target, AppServerTarget::Remote { .. }) {
|
||||
return AbsolutePathBuf::current_dir();
|
||||
}
|
||||
|
||||
match cwd {
|
||||
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?),
|
||||
None => AbsolutePathBuf::current_dir(),
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_session_cwd_filter<'a>(
|
||||
remote_mode: bool,
|
||||
remote_cwd_override: Option<&'a Path>,
|
||||
config: &'a Config,
|
||||
show_all: bool,
|
||||
) -> Option<&'a Path> {
|
||||
if show_all {
|
||||
return None;
|
||||
}
|
||||
|
||||
if remote_mode {
|
||||
remote_cwd_override
|
||||
} else {
|
||||
Some(config.cwd.as_path())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_main(
|
||||
mut cli: Cli,
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
@@ -604,6 +631,10 @@ pub async fn run_main(
|
||||
auth_token: remote_auth_token.clone(),
|
||||
})
|
||||
.unwrap_or(AppServerTarget::Embedded);
|
||||
let remote_cwd_override = cli
|
||||
.cwd
|
||||
.clone()
|
||||
.filter(|_| matches!(app_server_target, AppServerTarget::Remote { .. }));
|
||||
let (sandbox_mode, approval_policy) = if cli.full_auto {
|
||||
(
|
||||
Some(SandboxMode::WorkspaceWrite),
|
||||
@@ -654,10 +685,7 @@ pub async fn run_main(
|
||||
};
|
||||
|
||||
let cwd = cli.cwd.clone();
|
||||
let config_cwd = match cwd.as_deref() {
|
||||
Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?,
|
||||
None => AbsolutePathBuf::current_dir()?,
|
||||
};
|
||||
let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target)?;
|
||||
|
||||
#[allow(clippy::print_stderr)]
|
||||
let config_toml = match load_config_as_toml_with_cli_overrides(
|
||||
@@ -745,7 +773,11 @@ pub async fn run_main(
|
||||
model,
|
||||
approval_policy,
|
||||
sandbox_mode,
|
||||
cwd,
|
||||
cwd: if matches!(app_server_target, AppServerTarget::Remote { .. }) {
|
||||
None
|
||||
} else {
|
||||
cwd
|
||||
},
|
||||
model_provider: model_provider_override.clone(),
|
||||
config_profile: cli.config_profile.clone(),
|
||||
codex_self_exe: arg0_paths.codex_self_exe.clone(),
|
||||
@@ -907,6 +939,7 @@ pub async fn run_main(
|
||||
arg0_paths,
|
||||
loader_overrides,
|
||||
app_server_target,
|
||||
remote_cwd_override,
|
||||
config,
|
||||
overrides,
|
||||
cli_kv_overrides,
|
||||
@@ -925,6 +958,7 @@ async fn run_ratatui_app(
|
||||
arg0_paths: Arg0DispatchPaths,
|
||||
loader_overrides: LoaderOverrides,
|
||||
app_server_target: AppServerTarget,
|
||||
remote_cwd_override: Option<PathBuf>,
|
||||
initial_config: Config,
|
||||
overrides: ConfigOverrides,
|
||||
cli_kv_overrides: Vec<(String, toml::Value)>,
|
||||
@@ -983,18 +1017,21 @@ async fn run_ratatui_app(
|
||||
let needs_onboarding_app_server =
|
||||
should_show_trust_screen_flag || initial_config.model_provider.requires_openai_auth;
|
||||
let mut onboarding_app_server = if needs_onboarding_app_server {
|
||||
Some(AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
initial_config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
Some(
|
||||
AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
initial_config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
.with_remote_cwd_override(remote_cwd_override.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1097,18 +1134,21 @@ async fn run_ratatui_app(
|
||||
|| cli.resume_picker
|
||||
|| cli.fork_picker;
|
||||
let mut session_lookup_app_server = if needs_app_server_session_lookup {
|
||||
Some(AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
Some(
|
||||
AppServerSession::new(
|
||||
start_app_server(
|
||||
&app_server_target,
|
||||
arg0_paths.clone(),
|
||||
config.clone(),
|
||||
cli_kv_overrides.clone(),
|
||||
loader_overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
feedback.clone(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
.with_remote_cwd_override(remote_cwd_override.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1127,12 +1167,21 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
} else if cli.fork_last {
|
||||
let filter_cwd = if remote_mode {
|
||||
latest_session_cwd_filter(
|
||||
remote_mode,
|
||||
remote_cwd_override.as_deref(),
|
||||
&config,
|
||||
cli.fork_show_all,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --fork --last");
|
||||
};
|
||||
match lookup_latest_session_target_with_app_server(
|
||||
app_server, &config, /*cwd_filter*/ None,
|
||||
/*include_non_interactive*/ false,
|
||||
app_server, &config, filter_cwd, /*include_non_interactive*/ false,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
@@ -1179,11 +1228,12 @@ async fn run_ratatui_app(
|
||||
}
|
||||
}
|
||||
} else if cli.resume_last {
|
||||
let filter_cwd = if cli.resume_show_all {
|
||||
None
|
||||
} else {
|
||||
Some(config.cwd.as_path())
|
||||
};
|
||||
let filter_cwd = latest_session_cwd_filter(
|
||||
remote_mode,
|
||||
remote_cwd_override.as_deref(),
|
||||
&config,
|
||||
cli.resume_show_all,
|
||||
);
|
||||
let Some(app_server) = session_lookup_app_server.as_mut() else {
|
||||
unreachable!("session lookup app server should be initialized for --resume --last");
|
||||
};
|
||||
@@ -1334,7 +1384,7 @@ async fn run_ratatui_app(
|
||||
|
||||
let app_result = App::run(
|
||||
&mut tui,
|
||||
AppServerSession::new(app_server),
|
||||
AppServerSession::new(app_server).with_remote_cwd_override(remote_cwd_override),
|
||||
config,
|
||||
cli_kv_overrides.clone(),
|
||||
overrides.clone(),
|
||||
@@ -1795,12 +1845,9 @@ mod tests {
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let cwd = temp_dir.path().join("project");
|
||||
|
||||
let params = latest_session_lookup_params(
|
||||
/*is_remote*/ true,
|
||||
&config,
|
||||
Some(cwd.as_path()),
|
||||
/*is_remote*/ true, &config, /*cwd_filter*/ None,
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
@@ -1809,6 +1856,58 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions()
|
||||
-> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let cwd = Path::new("repo/on/server");
|
||||
|
||||
let params = latest_session_lookup_params(
|
||||
/*is_remote*/ true,
|
||||
&config,
|
||||
Some(cwd),
|
||||
/*include_non_interactive*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(params.model_providers, None);
|
||||
assert_eq!(params.cwd.as_deref(), Some("repo/on/server"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_cwd_for_app_server_target_uses_current_dir_for_remote_sessions() -> std::io::Result<()>
|
||||
{
|
||||
let remote_only_cwd = if cfg!(windows) {
|
||||
Path::new(r"C:\definitely\not\local\to\this\test")
|
||||
} else {
|
||||
Path::new("/definitely/not/local/to/this/test")
|
||||
};
|
||||
let target = AppServerTarget::Remote {
|
||||
websocket_url: "ws://127.0.0.1:1234/".to_string(),
|
||||
auth_token: None,
|
||||
};
|
||||
|
||||
let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target)?;
|
||||
|
||||
assert_eq!(config_cwd, AbsolutePathBuf::current_dir()?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let target = AppServerTarget::Embedded;
|
||||
|
||||
let config_cwd = config_cwd_for_app_server_target(Some(temp_dir.path()), &target)?;
|
||||
|
||||
assert_eq!(
|
||||
config_cwd,
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path().canonicalize()?)?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_returns_none_without_sqlite_or_rollout_path() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
@@ -172,13 +172,18 @@ pub async fn run_resume_picker_with_app_server(
|
||||
) -> Result<SessionSelection> {
|
||||
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
|
||||
let is_remote = app_server.is_remote();
|
||||
let cwd_filter = if show_all {
|
||||
None
|
||||
} else {
|
||||
app_server.remote_cwd_override().map(Path::to_path_buf)
|
||||
};
|
||||
run_session_picker_with_loader(
|
||||
tui,
|
||||
config,
|
||||
show_all,
|
||||
SessionPickerAction::Resume,
|
||||
is_remote,
|
||||
spawn_app_server_page_loader(app_server, include_non_interactive, bg_tx),
|
||||
spawn_app_server_page_loader(app_server, cwd_filter, include_non_interactive, bg_tx),
|
||||
bg_rx,
|
||||
)
|
||||
.await
|
||||
@@ -192,13 +197,20 @@ pub async fn run_fork_picker_with_app_server(
|
||||
) -> Result<SessionSelection> {
|
||||
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
|
||||
let is_remote = app_server.is_remote();
|
||||
let cwd_filter = if show_all {
|
||||
None
|
||||
} else {
|
||||
app_server.remote_cwd_override().map(Path::to_path_buf)
|
||||
};
|
||||
run_session_picker_with_loader(
|
||||
tui,
|
||||
config,
|
||||
show_all,
|
||||
SessionPickerAction::Fork,
|
||||
is_remote,
|
||||
spawn_app_server_page_loader(app_server, /*include_non_interactive*/ false, bg_tx),
|
||||
spawn_app_server_page_loader(
|
||||
app_server, cwd_filter, /*include_non_interactive*/ false, bg_tx,
|
||||
),
|
||||
bg_rx,
|
||||
)
|
||||
.await
|
||||
@@ -242,8 +254,8 @@ async fn run_session_picker_with_loader(
|
||||
let codex_home = config.codex_home.as_path();
|
||||
let filter_cwd = if show_all || is_remote {
|
||||
// Remote sessions live in the server's filesystem namespace, so the client
|
||||
// process cwd is not a meaningful default filter. A real remote cwd filter
|
||||
// would need an explicit server-side target cwd instead of current_dir().
|
||||
// process cwd is not a meaningful row filter. If the user provided an
|
||||
// explicit remote --cd, filtering is handled server-side in thread/list.
|
||||
None
|
||||
} else {
|
||||
std::env::current_dir().ok()
|
||||
@@ -341,6 +353,7 @@ fn spawn_rollout_page_loader(
|
||||
|
||||
fn spawn_app_server_page_loader(
|
||||
app_server: AppServerSession,
|
||||
cwd_filter: Option<PathBuf>,
|
||||
include_non_interactive: bool,
|
||||
bg_tx: mpsc::UnboundedSender<BackgroundEvent>,
|
||||
) -> PageLoader {
|
||||
@@ -357,6 +370,7 @@ fn spawn_app_server_page_loader(
|
||||
let page = load_app_server_page(
|
||||
&mut app_server,
|
||||
cursor,
|
||||
cwd_filter.as_deref(),
|
||||
request.provider_filter,
|
||||
request.sort_key,
|
||||
include_non_interactive,
|
||||
@@ -467,6 +481,7 @@ impl LoadingState {
|
||||
async fn load_app_server_page(
|
||||
app_server: &mut AppServerSession,
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
@@ -474,6 +489,7 @@ async fn load_app_server_page(
|
||||
let response = app_server
|
||||
.thread_list(thread_list_params(
|
||||
cursor,
|
||||
cwd_filter,
|
||||
provider_filter,
|
||||
sort_key,
|
||||
include_non_interactive,
|
||||
@@ -1101,6 +1117,7 @@ fn row_from_app_server_thread(thread: Thread) -> Option<Row> {
|
||||
|
||||
fn thread_list_params(
|
||||
cursor: Option<String>,
|
||||
cwd_filter: Option<&Path>,
|
||||
provider_filter: ProviderFilter,
|
||||
sort_key: ThreadSortKey,
|
||||
include_non_interactive: bool,
|
||||
@@ -1119,7 +1136,7 @@ fn thread_list_params(
|
||||
source_kinds: (!include_non_interactive)
|
||||
.then_some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
|
||||
archived: Some(false),
|
||||
cwd: None,
|
||||
cwd: cwd_filter.map(|cwd| cwd.to_string_lossy().to_string()),
|
||||
search_term: None,
|
||||
}
|
||||
}
|
||||
@@ -1836,6 +1853,7 @@ mod tests {
|
||||
fn remote_thread_list_params_omit_provider_filter() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
Some(Path::new("repo/on/server")),
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ false,
|
||||
@@ -1847,12 +1865,14 @@ mod tests {
|
||||
params.source_kinds,
|
||||
Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode])
|
||||
);
|
||||
assert_eq!(params.cwd.as_deref(), Some("repo/on/server"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_thread_list_params_can_include_non_interactive_sources() {
|
||||
let params = thread_list_params(
|
||||
Some(String::from("cursor-1")),
|
||||
/*cwd_filter*/ None,
|
||||
ProviderFilter::Any,
|
||||
ThreadSortKey::UpdatedAt,
|
||||
/*include_non_interactive*/ true,
|
||||
|
||||
Reference in New Issue
Block a user