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
17 changes: 13 additions & 4 deletions app/src/ai/agent_sdk/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,12 @@ impl EnvironmentCommandRunner {
println!("Description: {desc}");
}
match &env.base_image {
BaseImage::DockerImage(img) => {
Some(BaseImage::DockerImage(img)) => {
println!("Docker image: {img}");
}
None => {
println!("Docker image: None");
}
}
if env.github_repos.is_empty() {
println!("Repositories: None");
Expand Down Expand Up @@ -952,7 +955,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 {
Expand Down Expand Up @@ -1121,7 +1124,8 @@ struct EnvironmentInfo {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
base_image: BaseImage,
#[serde(skip_serializing_if = "Option::is_none")]
base_image: Option<BaseImage>,
github_repos: Vec<GithubRepo>,
setup_commands: Vec<String>,
creator_email: String,
Expand Down Expand Up @@ -1156,12 +1160,17 @@ impl TableFormat for EnvironmentInfo {
.join(", ");
let setup_commands_display = self.setup_commands.join("\n");
let description_display = self.description.as_deref().unwrap_or("");
let base_image_display = self
.base_image
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "None".to_string());

vec![
Cell::new(&self.id),
Cell::new(&self.name),
Cell::new(description_display),
Cell::new(self.base_image.to_string()),
Cell::new(base_image_display),
Cell::new(github_repos_display),
Cell::new(setup_commands_display),
Cell::new(&self.creator_email),
Expand Down
34 changes: 19 additions & 15 deletions app/src/ai/conversation_details_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1635,7 +1635,7 @@ impl ConversationDetailsPanel {
app: &AppContext,
) -> Box<dyn Element> {
let environment_name = &env_model.name;
let docker_image = env_model.base_image.to_string();
let docker_image = env_model.base_image.as_ref().map(ToString::to_string);

let theme = appearance.theme();
let ui_font_size = appearance.ui_font_size();
Expand Down Expand Up @@ -1702,16 +1702,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(docker_image) = &docker_image {
section.add_child(
Container::new(render_copyable_field(
"Image",
docker_image,
CopyButtonKind::DockerImage,
ConversationDetailsPanelAction::CopyDockerImage,
))
.with_margin_bottom(LABEL_VALUE_GAP)
.finish(),
);
}

Container::new(section.finish())
.with_margin_bottom(FIELD_SPACING)
Expand Down Expand Up @@ -2263,10 +2265,12 @@ impl TypedActionView for ConversationDetailsPanel {
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();
ctx.clipboard()
.write(ClipboardContent::plain_text(docker_image));
self.record_copy(CopyButtonKind::DockerImage, ctx);
if let Some(base_image) = &env.model().string_model.base_image {
let docker_image = base_image.to_string();
ctx.clipboard()
.write(ClipboardContent::plain_text(docker_image));
self.record_copy(CopyButtonKind::DockerImage, ctx);
}
}
}
}
Expand Down
17 changes: 11 additions & 6 deletions app/src/context_chips/display_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ struct FilteredMenuItem {
struct EnvironmentSidecarData {
name: String,
id: String,
image: String,
image: Option<String>,
repos_text: String,
}

Expand Down Expand Up @@ -664,7 +664,12 @@ 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),
repos_text,
})
}
Expand Down Expand Up @@ -824,9 +829,9 @@ impl DisplayChipMenu {
)
};

let image_value = {
let docker_image = data.image.clone();
render_copyable_text_field(
let image_value = match data.image.clone() {
None => value_text("(none)".to_string()),
Some(docker_image) => render_copyable_text_field(
CopyableTextFieldConfig::new(docker_image.clone())
.with_font_size(value_font_size)
.with_text_color(main_text_color)
Expand All @@ -843,7 +848,7 @@ impl DisplayChipMenu {
});
},
app,
)
),
};

let row = |row_icon: Icon, label: &str, value: Box<dyn Element>, is_last: bool| {
Expand Down
17 changes: 12 additions & 5 deletions app/src/settings_view/environments_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ struct EnvironmentDisplayData {
id: SyncId,
name: String,
description: Option<String>,
docker_image: String,
docker_image: Option<String>,
github_repos: Vec<(String, String)>, // (owner, repo)
setup_commands: Vec<String>,
last_edited_ts: Option<ServerTimestamp>,
Expand All @@ -144,7 +144,7 @@ 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),
github_repos: model
.github_repos
.iter()
Expand All @@ -168,7 +168,7 @@ impl EnvironmentDisplayData {
self.id.to_string(),
self.name.clone(),
self.description.clone().unwrap_or_default(),
self.docker_image.clone(),
self.docker_image.clone().unwrap_or_default(),
];

if haystacks
Expand Down Expand Up @@ -270,7 +270,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(),
}
});
Expand Down Expand Up @@ -1811,7 +1815,10 @@ impl EnvironmentsPageWidget {
}
}

let mut details_parts = vec![format!("Image: {}", env_docker_image)];
let mut details_parts = Vec::new();
if let Some(image) = &env_docker_image {
details_parts.push(format!("Image: {image}"));
}

if !env_github_repos.is_empty() {
let repos_text = env_github_repos
Expand Down
2 changes: 1 addition & 1 deletion app/src/settings_view/environments_page_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn make_test_environment_with_timestamps(
id: SyncId::ClientId(ClientId::new()),
name: name.to_string(),
description: None,
docker_image: docker_image.to_string(),
docker_image: Some(docker_image.to_string()),
github_repos,
setup_commands,
last_edited_ts,
Expand Down
11 changes: 8 additions & 3 deletions crates/cloud_object_models/src/cloud_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,14 @@ pub struct AmbientAgentEnvironment {
/// `github_repos`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_repos: Option<Vec<SourceRepo>>,
/// Base image specification
/// Base image specification.
///
/// `None` means the environment does not pin a base image (the key is
/// absent from the stored JSON); the server picks a default in that case.
/// Serialization emits no fields for `None`, preserving the existing wire
/// format for environments that do pin an image.
#[serde(flatten)]
pub base_image: BaseImage,
pub base_image: Option<BaseImage>,
/// List of setup commands to run after cloning
#[serde(default)]
pub setup_commands: Vec<String>,
Expand Down Expand Up @@ -212,7 +217,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,
Expand Down
68 changes: 67 additions & 1 deletion crates/cloud_object_models/src/cloud_environment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,77 @@ 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_docker_image() {
let json = serde_json::json!({
"name": "no-image-env",
"github_repos": [{"owner": "warpdotdev", "repo": "warp"}],
"setup_commands": ["echo hello"]
});

let env: AmbientAgentEnvironment = serde_json::from_value(json).unwrap();
assert_eq!(env.name, "no-image-env");
assert_eq!(env.base_image, None);
assert_eq!(env.github_repos.len(), 1);
assert_eq!(env.setup_commands, vec!["echo hello"]);
}

#[test]
fn serialize_environment_without_docker_image_omits_field() {
let mut env = AmbientAgentEnvironment::new(
"no-image-env".into(),
None,
vec![],
"ubuntu:latest".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 serialize_environment_with_docker_image_preserves_wire_format() {
let env = AmbientAgentEnvironment::new(
"image-env".into(),
None,
vec![],
"ubuntu:latest".into(),
vec![],
);

let json = serde_json::to_value(&env).unwrap();
// The base image must serialize as a flat top-level `docker_image` key.
assert_eq!(
json.get("docker_image"),
Some(&serde_json::json!("ubuntu:latest"))
);
assert!(!json.as_object().unwrap().contains_key("base_image"));
}

#[test]
fn roundtrip_serde_without_docker_image() {
let mut env = AmbientAgentEnvironment::new(
"no-image-rt".into(),
Some("desc".into()),
vec![GithubRepo::new("owner".into(), "repo".into())],
"unused".into(),
vec!["make build".into()],
);
env.base_image = None;

let serialized = serde_json::to_string(&env).unwrap();
let deserialized: AmbientAgentEnvironment = serde_json::from_str(&serialized).unwrap();
assert_eq!(env, deserialized);
assert_eq!(deserialized.base_image, None);
}

#[test]
fn deserialize_gitlab_environment_uses_authoritative_source_repos() {
let json = serde_json::json!({
Expand Down