Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions app/src/ai/agent_sdk/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
}
Expand Down
34 changes: 26 additions & 8 deletions app/src/settings_view/environments_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions crates/cloud_object_models/src/cloud_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GithubRepo> {
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<SourceRepo> {
let code_forge = self.effective_code_forge();
Expand Down