From 995b83df24660eab14c18dd16829d61ff81060c7 Mon Sep 17 00:00:00 2001 From: liliwilson Date: Mon, 13 Jul 2026 21:47:12 +0000 Subject: [PATCH 1/2] Fix env edit to preserve server fields and show repos from source_repos When editing an environment via the UI: - Merge form changes into the existing environment model instead of creating a fresh one, preserving server-managed fields (secrets, providers, code_forge) that the form doesn't expose. - Source_repos is cleared so the server falls back to github_repos (which the form now correctly populates from effective_github_repos). - Init the repos field from effective_github_repos() so environments that store repos via source_repos show them in the edit form. - Hide 'Image:' label on environment cards when no docker image is set. These address remaining gaps after #13553 for environments without a docker image: the form now correctly shows and saves their repos. Co-Authored-By: Oz --- app/src/settings_view/environments_page.rs | 34 ++++++++++++++----- .../src/cloud_environment.rs | 26 ++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/src/settings_view/environments_page.rs b/app/src/settings_view/environments_page.rs index 983fa7ce952..0e46cb1302d 100644 --- a/app/src/settings_view/environments_page.rs +++ b/app/src/settings_view/environments_page.rs @@ -269,7 +269,9 @@ impl EnvironmentsPageView { EnvironmentFormValues { name: model.name.clone(), description: model.description.clone().unwrap_or_default(), - selected_repos: model.github_repos.clone(), + // Use effective_github_repos so environments that store repos + // via source_repos (newer path) still show them in the form. + selected_repos: model.effective_github_repos(), docker_image: model.base_image_display(), setup_commands: model.setup_commands.clone(), } @@ -799,17 +801,30 @@ impl EnvironmentsPageView { // Get the revision from the existing environment let revision = existing_env.metadata.revision.clone(); + // Merge form changes into the existing environment so server-managed fields + // (secrets, providers, source_repos, code_forge) are preserved. The form only + // exposes name, description, github_repos, docker_image, and setup_commands; + // all other fields are kept from the current server state. + let mut merged = existing_env.model().string_model.clone(); + merged.name = environment.name.clone(); + merged.description = environment.description.clone(); + merged.base_image = environment.base_image.clone(); + merged.setup_commands = environment.setup_commands.clone(); + // Replace github_repos with the form's selection. When the existing environment + // uses source_repos as its authoritative repo list, clear it so the server falls + // back to github_repos; this prevents the new repo selection from being silently + // ignored by the server. + merged.github_repos = environment.github_repos.clone(); + if merged.source_repos.is_some() { + merged.source_repos = None; + } + // Track the pending save to show success toast when complete self.pending_save_env_id = Some(*env_id); // Update via UpdateManager UpdateManager::handle(ctx).update(ctx, |update_manager, ctx| { - update_manager.update_ambient_agent_environment( - environment.clone(), - *env_id, - revision, - ctx, - ); + update_manager.update_ambient_agent_environment(merged, *env_id, revision, ctx); }); // Navigate back to list @@ -1811,7 +1826,10 @@ impl EnvironmentsPageWidget { } } - let mut details_parts = vec![format!("Image: {}", env_docker_image)]; + let mut details_parts = Vec::new(); + if !env_docker_image.is_empty() { + details_parts.push(format!("Image: {}", env_docker_image)); + } if !env_github_repos.is_empty() { let repos_text = env_github_repos diff --git a/crates/cloud_object_models/src/cloud_environment.rs b/crates/cloud_object_models/src/cloud_environment.rs index 6e0384f1d3a..7c3042fad3b 100644 --- a/crates/cloud_object_models/src/cloud_environment.rs +++ b/crates/cloud_object_models/src/cloud_environment.rs @@ -238,6 +238,32 @@ impl AmbientAgentEnvironment { .unwrap_or_default() } + /// Returns the effective GitHub-compatible repo list for display and editing. + /// + /// When `source_repos` is set and contains only GitHub repos, those are + /// returned (converted to the simpler `GithubRepo` type). Otherwise falls + /// back to `github_repos`. This lets the edit form show the correct repo + /// list for environments that were created via the newer `source_repos` path. + pub fn effective_github_repos(&self) -> Vec { + let code_forge = self.effective_code_forge(); + match &self.source_repos { + Some(source_repos) => { + let all_github = source_repos + .iter() + .all(|r| r.code_forge.unwrap_or(code_forge) == CodeForge::GitHub); + if all_github { + source_repos + .iter() + .map(|r| GithubRepo::new(r.owner.clone(), r.repo.clone())) + .collect() + } else { + self.github_repos.clone() + } + } + None => self.github_repos.clone(), + } + } + /// Returns the authoritative provider-neutral repository list. pub fn effective_repos(&self) -> Vec { let code_forge = self.effective_code_forge(); From 90d8024b639ebde3fbb71c0f69146d051131c533 Mon Sep 17 00:00:00 2001 From: liliwilson Date: Mon, 13 Jul 2026 22:02:31 +0000 Subject: [PATCH 2/2] Fix CLI env update: use effective repos and clear source_repos The server uses source_repos as the authoritative repo list when present. When updating only github_repos (the legacy field) while source_repos is non-nil in the payload, the server ignores github_repos and the changes are silently dropped. Use effective_github_repos() to compute the canonical list (reading from source_repos when set), apply adds/removes to it, write back to github_repos, and clear source_repos so the server falls back to github_repos as authoritative. This is the root cause of David's reported issue where CLI repo updates appeared to succeed but repos were not present on re-fetch. Co-Authored-By: Oz --- app/src/ai/agent_sdk/environment.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/ai/agent_sdk/environment.rs b/app/src/ai/agent_sdk/environment.rs index 0ad1a24e6ef..602b24088d5 100644 --- a/app/src/ai/agent_sdk/environment.rs +++ b/app/src/ai/agent_sdk/environment.rs @@ -958,15 +958,26 @@ impl EnvironmentCommandRunner { updated_env.base_image = Some(BaseImage::DockerImage(new_docker_image)); } + // The server writes repos into *both* source_repos (authoritative) and + // github_repos (legacy mirror). When we clone the existing model and + // modify only github_repos, the serialized update still contains the + // original source_repos, which the server then treats as authoritative + // — silently dropping any github_repos changes. Fix: use + // effective_github_repos() to get the canonical repo list (from + // source_repos when present, falling back to github_repos), apply + // adds/removes on that list, write back into github_repos, and clear + // source_repos so the server falls back to github_repos. + let mut effective = updated_env.effective_github_repos(); + for repo in add_repos { - if !updated_env.github_repos.contains(&repo) { - updated_env.github_repos.push(repo); + if !effective.contains(&repo) { + effective.push(repo); } } for repo in &remove_repos { - if let Some(pos) = updated_env.github_repos.iter().position(|r| r == repo) { - updated_env.github_repos.remove(pos); + if let Some(pos) = effective.iter().position(|r| r == repo) { + effective.remove(pos); } else { eprintln!( "Warning: repository {}/{} not found in environment, skipping removal", @@ -975,6 +986,10 @@ impl EnvironmentCommandRunner { } } + updated_env.github_repos = effective; + // Clear source_repos so the server falls back to github_repos. + updated_env.source_repos = None; + for cmd in add_setup_commands { updated_env.setup_commands.push(cmd); }