diff --git a/app/src/ai/agent_sdk/environment.rs b/app/src/ai/agent_sdk/environment.rs index 425cb999f89..b4958e6e8a0 100644 --- a/app/src/ai/agent_sdk/environment.rs +++ b/app/src/ai/agent_sdk/environment.rs @@ -300,10 +300,8 @@ impl EnvironmentCommandRunner { if let Some(desc) = &env.description { println!("Description: {desc}"); } - match &env.base_image { - BaseImage::DockerImage(img) => { - println!("Docker image: {img}"); - } + if let Some(BaseImage::DockerImage(img)) = &env.base_image { + println!("Docker image: {img}"); } if env.github_repos.is_empty() { println!("Repositories: None"); @@ -952,7 +950,7 @@ impl EnvironmentCommandRunner { } if let Some(new_docker_image) = docker_image { - updated_env.base_image = BaseImage::DockerImage(new_docker_image); + updated_env.base_image = Some(BaseImage::DockerImage(new_docker_image)); } for repo in add_repos { @@ -1121,7 +1119,8 @@ struct EnvironmentInfo { name: String, #[serde(skip_serializing_if = "Option::is_none")] description: Option, - base_image: BaseImage, + #[serde(skip_serializing_if = "Option::is_none")] + base_image: Option, github_repos: Vec, setup_commands: Vec, creator_email: String, @@ -1161,7 +1160,12 @@ impl TableFormat for EnvironmentInfo { Cell::new(&self.id), Cell::new(&self.name), Cell::new(description_display), - Cell::new(self.base_image.to_string()), + Cell::new( + self.base_image + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), + ), Cell::new(github_repos_display), Cell::new(setup_commands_display), Cell::new(&self.creator_email), diff --git a/app/src/ai/conversation_details_panel.rs b/app/src/ai/conversation_details_panel.rs index 51ba6c9776f..49f6494f45e 100644 --- a/app/src/ai/conversation_details_panel.rs +++ b/app/src/ai/conversation_details_panel.rs @@ -1635,7 +1635,6 @@ impl ConversationDetailsPanel { app: &AppContext, ) -> Box { let environment_name = &env_model.name; - let docker_image = env_model.base_image.to_string(); let theme = appearance.theme(); let ui_font_size = appearance.ui_font_size(); @@ -1702,16 +1701,18 @@ impl ConversationDetailsPanel { .finish(), ); - section.add_child( - Container::new(render_copyable_field( - "Image", - &docker_image, - CopyButtonKind::DockerImage, - ConversationDetailsPanelAction::CopyDockerImage, - )) - .with_margin_bottom(LABEL_VALUE_GAP) - .finish(), - ); + if let Some(base_image) = &env_model.base_image { + section.add_child( + Container::new(render_copyable_field( + "Image", + &base_image.to_string(), + CopyButtonKind::DockerImage, + ConversationDetailsPanelAction::CopyDockerImage, + )) + .with_margin_bottom(LABEL_VALUE_GAP) + .finish(), + ); + } Container::new(section.finish()) .with_margin_bottom(FIELD_SPACING) @@ -2262,8 +2263,15 @@ impl TypedActionView for ConversationDetailsPanel { // Fetch docker image from environment if let Ok(server_id) = ServerId::try_from(env_id.as_str()) { let sync_id = SyncId::ServerId(server_id); - if let Some(env) = CloudAmbientAgentEnvironment::get_by_id(&sync_id, ctx) { - let docker_image = env.model().string_model.base_image.to_string(); + let docker_image = CloudAmbientAgentEnvironment::get_by_id(&sync_id, ctx) + .and_then(|env| { + env.model() + .string_model + .base_image + .as_ref() + .map(ToString::to_string) + }); + if let Some(docker_image) = docker_image { ctx.clipboard() .write(ClipboardContent::plain_text(docker_image)); self.record_copy(CopyButtonKind::DockerImage, ctx); diff --git a/app/src/context_chips/display_menu.rs b/app/src/context_chips/display_menu.rs index 41513559d52..d88bf8e61f1 100644 --- a/app/src/context_chips/display_menu.rs +++ b/app/src/context_chips/display_menu.rs @@ -664,7 +664,13 @@ impl DisplayChipMenu { Some(EnvironmentSidecarData { name: env.model().string_model.display_name(), id: env.id.to_string(), - image: env.model().string_model.base_image.to_string(), + image: env + .model() + .string_model + .base_image + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), repos_text, }) } diff --git a/app/src/settings_view/environments_page.rs b/app/src/settings_view/environments_page.rs index 84265303660..07e8bc87708 100644 --- a/app/src/settings_view/environments_page.rs +++ b/app/src/settings_view/environments_page.rs @@ -144,7 +144,11 @@ impl EnvironmentDisplayData { id: env.id, name: model.name.clone(), description: model.description.clone(), - docker_image: model.base_image.to_string(), + docker_image: model + .base_image + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), github_repos: model .github_repos .iter() @@ -270,7 +274,11 @@ impl EnvironmentsPageView { name: model.name.clone(), description: model.description.clone().unwrap_or_default(), selected_repos: model.github_repos.clone(), - docker_image: model.base_image.to_string(), + docker_image: model + .base_image + .as_ref() + .map(ToString::to_string) + .unwrap_or_default(), setup_commands: model.setup_commands.clone(), } }); diff --git a/crates/cloud_object_models/src/cloud_environment.rs b/crates/cloud_object_models/src/cloud_environment.rs index 0a23d4f6c13..e7926df4675 100644 --- a/crates/cloud_object_models/src/cloud_environment.rs +++ b/crates/cloud_object_models/src/cloud_environment.rs @@ -181,9 +181,9 @@ pub struct AmbientAgentEnvironment { /// `github_repos`. #[serde(default, skip_serializing_if = "Option::is_none")] pub source_repos: Option>, - /// Base image specification - #[serde(flatten)] - pub base_image: BaseImage, + /// Optional base image specification + #[serde(flatten, default, skip_serializing_if = "Option::is_none")] + pub base_image: Option, /// List of setup commands to run after cloning #[serde(default)] pub setup_commands: Vec, @@ -212,7 +212,7 @@ impl AmbientAgentEnvironment { code_forge: None, github_repos, source_repos: None, - base_image: BaseImage::DockerImage(docker_image), + base_image: Some(BaseImage::DockerImage(docker_image)), setup_commands, providers: ProvidersConfig::default(), secrets: None, diff --git a/crates/cloud_object_models/src/cloud_environment_tests.rs b/crates/cloud_object_models/src/cloud_environment_tests.rs index 74352cdc331..31f6c67c551 100644 --- a/crates/cloud_object_models/src/cloud_environment_tests.rs +++ b/crates/cloud_object_models/src/cloud_environment_tests.rs @@ -28,11 +28,63 @@ fn deserialize_legacy_environment_without_providers() { ); assert_eq!( env.base_image, - BaseImage::DockerImage("ubuntu:latest".into()) + Some(BaseImage::DockerImage("ubuntu:latest".into())) ); assert_eq!(env.setup_commands, vec!["echo hello"]); } +#[test] +fn deserialize_environment_without_base_image() { + let json = serde_json::json!({ + "name": "default-image-env", + "github_repos": [], + "setup_commands": [] + }); + + let env: AmbientAgentEnvironment = serde_json::from_value(json).unwrap(); + + assert_eq!(env.base_image, None); +} + +#[test] +fn base_image_serialization_preserves_wire_format() { + let env = AmbientAgentEnvironment::new( + "pinned-image-env".into(), + None, + vec![], + "ubuntu:latest".into(), + vec![], + ); + + let json = serde_json::to_value(env).unwrap(); + + assert_eq!( + json, + serde_json::json!({ + "name": "pinned-image-env", + "github_repos": [], + "docker_image": "ubuntu:latest", + "setup_commands": [] + }) + ); +} + +#[test] +fn serialize_environment_without_base_image_omits_docker_image() { + let mut env = AmbientAgentEnvironment::new( + "default-image-env".into(), + None, + vec![], + "unused".into(), + vec![], + ); + env.base_image = None; + + let json = serde_json::to_value(env).unwrap(); + + assert!(!json.as_object().unwrap().contains_key("docker_image")); +} + #[test] fn deserialize_gitlab_environment_uses_authoritative_source_repos() { let json = serde_json::json!({