From 0ab8eda375b60dbefd9bf30f2c37d090643866ee Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 3 Apr 2026 11:26:45 -0700 Subject: [PATCH] Add remote --cd forwarding for app-server sessions (#16700) Addresses #16124 Problem: `codex --remote --cd ` 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. --- codex-rs/app-server/README.md | 2 +- .../app-server/src/codex_message_processor.rs | 60 +++++- codex-rs/tui/src/app_server_session.rs | 100 ++++++++-- codex-rs/tui/src/cli.rs | 1 + codex-rs/tui/src/lib.rs | 187 +++++++++++++----- codex-rs/tui/src/resume_picker.rs | 30 ++- 6 files changed, 317 insertions(+), 63 deletions(-) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index fdd61fba4..c24c3fd7e 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -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. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 0750cfd67..ff1dddf2f 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -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, +) -> Result, 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, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index c0be4a94f..08e8d237b 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -104,6 +104,7 @@ pub(crate) struct AppServerBootstrap { pub(crate) struct AppServerSession { client: AppServerClient, next_request_id: i64, + remote_cwd_override: Option, } #[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) -> 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 { +fn thread_cwd_from_config( + config: &Config, + thread_params_mode: ThreadParamsMode, + remote_cwd_override: Option<&std::path::Path>, +) -> Option { 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"); diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index e3087af2a..d1cbabf37 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -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, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 4c31516b5..991c91c55 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -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 { + 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, 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()?; diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index ba399521d..a415a2552 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -172,13 +172,18 @@ pub async fn run_resume_picker_with_app_server( ) -> Result { 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 { 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, include_non_interactive: bool, bg_tx: mpsc::UnboundedSender, ) -> 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, + 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 { fn thread_list_params( cursor: Option, + 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,