Improve orchestration agent model selection

This commit is contained in:
2026-08-26 02:15:52 -05:00
parent 92ab03be07
commit 2022716f91
18 changed files with 486 additions and 60 deletions
@@ -1247,7 +1247,7 @@ pub fn compose_run_agents_child_prompt(base_prompt: &str, per_agent_prompt: &str
}
}
/// Translates run-wide config into a per-child
/// Translates shared config and the child's model selection into a per-child
/// [`StartAgentExecutionMode`]. Returns `Err` for rejected
/// combinations (e.g. OpenCode+Remote).
///
@@ -1262,12 +1262,15 @@ pub fn run_agents_to_start_agent_mode(
run_auth_secret_name: Option<&str>,
cfg: &RunAgentsAgentRunConfig,
) -> Result<StartAgentExecutionMode, String> {
let child_model_id = if cfg.model_id.trim().is_empty() {
run_model_id.trim()
} else {
cfg.model_id.trim()
};
match run_execution_mode {
RunAgentsExecutionMode::Local => {
let trimmed = run_harness_type.trim();
// Propagate run-wide model selection for local launches.
let trimmed_model_id = run_model_id.trim();
let model_id = (!trimmed_model_id.is_empty()).then(|| trimmed_model_id.to_string());
let model_id = (!child_model_id.is_empty()).then(|| child_model_id.to_string());
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("oz") {
Ok(StartAgentExecutionMode::Local {
harness_type: None,
@@ -1299,7 +1302,7 @@ pub fn run_agents_to_start_agent_mode(
Ok(StartAgentExecutionMode::Remote {
environment_id: environment_id.clone(),
skill_references: run_skills.to_vec(),
model_id: run_model_id.to_string(),
model_id: child_model_id.to_string(),
computer_use_enabled: *computer_use_enabled,
worker_host: worker_host.clone(),
harness_type: run_harness_type.to_string(),
@@ -266,6 +266,7 @@ fn execute_denies_mixed_batch_containing_launched_agent() {
name: "new-child".to_string(),
prompt: "Do separate work".to_string(),
title: String::new(),
model_id: String::new(),
});
let execution = state.executor.update(&mut app, |executor, ctx| {
@@ -306,6 +307,7 @@ fn validate_request_rejects_blank_and_duplicate_agent_names() {
name: " child ".to_string(),
prompt: "Do separate work".to_string(),
title: String::new(),
model_id: String::new(),
});
assert_eq!(
validate_request(&request),
@@ -323,6 +325,7 @@ fn validate_request_allows_unique_sibling_names() {
name: "second-child".to_string(),
prompt: "Do separate work".to_string(),
title: String::new(),
model_id: String::new(),
});
assert_eq!(validate_request(&request), Ok(()));
@@ -384,6 +387,7 @@ fn recovered_run_agents_reattaches_existing_child_and_launches_only_missing_chil
name: "missing-child".to_string(),
prompt: "Do separate work".to_string(),
title: String::new(),
model_id: String::new(),
});
state.executor.update(&mut app, |executor, _| {
executor
@@ -454,12 +458,12 @@ fn recovered_run_agents_reattaches_existing_child_and_launches_only_missing_chil
assert_eq!(agents.len(), 2);
assert!(matches!(
&agents[0].kind,
RunAgentsAgentOutcomeKind::Launched { agent_id }
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
if agent_id == &existing_child_id.to_string()
));
assert!(matches!(
&agents[1].kind,
RunAgentsAgentOutcomeKind::Launched { agent_id }
RunAgentsAgentOutcomeKind::Completed { agent_id, .. }
if agent_id == &missing_child_id.to_string()
));
});
@@ -766,6 +770,7 @@ fn remote_run_agents_action(harness_type: &str) -> AIAgentAction {
name: "child".to_string(),
prompt: "Help".to_string(),
title: String::new(),
model_id: String::new(),
}],
plan_id: String::new(),
harness_auth_secret_name: None,
@@ -789,6 +794,7 @@ fn local_codex_run_agents_maps_to_local_harness_mode_when_flag_enabled() {
name: "child".to_string(),
prompt: "Investigate the failure".to_string(),
title: String::new(),
model_id: String::new(),
};
let mode = run_agents_to_start_agent_mode(
@@ -810,6 +816,34 @@ fn local_codex_run_agents_maps_to_local_harness_mode_when_flag_enabled() {
);
}
#[test]
fn child_model_overrides_the_run_wide_model() {
let cfg = RunAgentsAgentRunConfig {
name: "child".to_string(),
prompt: "Investigate".to_string(),
title: String::new(),
model_id: "child-model".to_string(),
};
let mode = run_agents_to_start_agent_mode(
&RunAgentsExecutionMode::Local,
"oz",
"fallback-model",
&[],
None,
&cfg,
)
.unwrap();
assert_eq!(
mode,
StartAgentExecutionMode::Local {
harness_type: None,
model_id: Some("child-model".to_string()),
}
);
}
fn persist_default_auth_secret(app: &mut App, harness_config_name: &str, secret_name: &str) {
CloudAgentSettings::handle(app).update(app, |settings, ctx| {
let mut secrets = settings.last_selected_auth_secret.value().clone();
+1
View File
@@ -483,6 +483,7 @@ fn agent_cfg() -> RunAgentsAgentRunConfig {
name: "child".to_string(),
prompt: "do X".to_string(),
title: "Child".to_string(),
model_id: String::new(),
}
}
+24 -6
View File
@@ -81,10 +81,11 @@ use crate::ai::provider::ProviderConfig;
#[cfg(not(target_family = "wasm"))]
use crate::ai::remote_logging::{self, RemoteLogLevel, RemoteLogRecord};
use crate::ai::runtime::{
prepare_provider_run, provider_runtime_for_request, PreparedProviderRun, ProviderActionContext,
ProviderRunBlock, ProviderRunCoordinator, ProviderRunProfile, ProviderRunProjection,
ProviderRunResponseProjector, ProviderToolExecutionRef, ProviderToolLifecycleOutcome,
RuntimeResponseConfig, BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
prepare_provider_run, provider_runtime_for_request, OrchestrationModelOption,
PreparedProviderRun, ProviderActionContext, ProviderRunBlock, ProviderRunCoordinator,
ProviderRunProfile, ProviderRunProjection, ProviderRunResponseProjector,
ProviderToolExecutionRef, ProviderToolLifecycleOutcome, RuntimeResponseConfig,
BASE_PROVIDER_PROFILE, CLI_MONITOR_PROVIDER_PROFILE,
};
use crate::ai::AIRequestUsageModel;
use crate::cloud_object::model::persistence::CloudModel;
@@ -6296,10 +6297,27 @@ impl BlocklistAIController {
request_params: api::RequestParams,
ctx: &mut ModelContext<Self>,
) {
let orchestration_models = LLMPreferences::as_ref(ctx)
.get_base_llm_choices_for_agent_mode(ctx)
.filter(|model| model.disable_reason.is_none())
.map(|model| OrchestrationModelOption {
id: model.id.to_string(),
display_name: model.display_name.clone(),
provider: model.provider.display_name().to_string(),
quality: model.spec.as_ref().map(|spec| spec.quality),
cost: model.spec.as_ref().map(|spec| spec.cost),
credit_multiplier: model.usage_metadata.credit_multiplier,
})
.collect();
let _ = ctx.spawn(
async move {
prepare_provider_run(base_provider_config, cli_provider_config, request_params)
.await
prepare_provider_run(
base_provider_config,
cli_provider_config,
request_params,
orchestration_models,
)
.await
},
move |me, result, ctx| {
me.handle_prepared_provider_run(conversation_id, stream_id, result, ctx);
@@ -430,6 +430,30 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
harness_type: &str,
is_local: bool,
ctx: &mut ViewContext<V>,
) {
populate_model_picker_for_harness_with_action(
dropdown,
initial_model_id,
harness_type,
is_local,
A::model_changed,
ctx,
);
}
/// Populates an orchestration model picker while allowing the caller to
/// attach extra identity (for example, a child-agent name) to model actions.
pub fn populate_model_picker_for_harness_with_action<
A: OrchestrationControlAction,
V: View,
F: Fn(String) -> A + Clone,
>(
dropdown: &ViewHandle<FilterableDropdown<A>>,
initial_model_id: &str,
harness_type: &str,
is_local: bool,
model_changed: F,
ctx: &mut ViewContext<V>,
) {
let initial_model_id = initial_model_id.to_string();
let harness_type = harness_type.to_string();
@@ -459,9 +483,7 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
let items = available_model_menu_items(
ordered_choices,
move |llm| {
DropdownAction::select_action_and_close(A::model_changed(
llm.id.to_string(),
))
DropdownAction::select_action_and_close(model_changed(llm.id.to_string()))
},
None,
None,
@@ -476,21 +498,22 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
}
Some(Harness::Codex) if is_local => {
// Local Codex: only "Default model" entry.
let items = vec![default_model_menu_item::<A>()];
let items = vec![default_model_menu_item(model_changed)];
dropdown.set_rich_items(items, ctx_dropdown);
dropdown.set_selected_by_name(DEFAULT_MODEL_LABEL, ctx_dropdown);
}
Some(harness) => {
// Non-Oz harness: "Default model" at top, then server-provided
// harness models.
let mut items: Vec<MenuItem<DropdownAction>> = vec![default_model_menu_item::<A>()];
let mut items: Vec<MenuItem<DropdownAction>> =
vec![default_model_menu_item(model_changed.clone())];
let availability = HarnessAvailabilityModel::as_ref(ctx_dropdown);
if let Some(models) = availability.models_for(harness) {
for model in models {
let model_id = model.id.clone();
let fields = MenuItemFields::new(&model.display_name)
.with_on_select_action(DropdownAction::select_action_and_close(
A::model_changed(model_id),
model_changed(model_id),
));
items.push(MenuItem::Item(fields));
}
@@ -519,10 +542,12 @@ pub fn populate_model_picker_for_harness<A: OrchestrationControlAction, V: View>
}
/// Creates a "Default model" menu item that emits an empty model_id.
fn default_model_menu_item<A: OrchestrationControlAction>() -> MenuItem<DropdownAction> {
fn default_model_menu_item<A: OrchestrationControlAction>(
model_changed: impl Fn(String) -> A,
) -> MenuItem<DropdownAction> {
MenuItem::Item(
MenuItemFields::new(DEFAULT_MODEL_LABEL).with_on_select_action(
DropdownAction::select_action_and_close(A::model_changed(String::new())),
DropdownAction::select_action_and_close(model_changed(String::new())),
),
)
}
@@ -598,6 +623,7 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
let harnesses = availability.available_harnesses();
let resolve_entry_harness = |harness: Harness, display_name: &str| match harness {
Harness::Unknown if display_name == "Warp" => Harness::Oz,
Harness::Unknown => [
Harness::Oz,
Harness::Claude,
@@ -644,11 +670,15 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
} else {
None
};
// Use the server-provided display_name for the label so stale
// cache entries (where harness deserializes as Unknown) still
// show the correct name.
let mut fields = MenuItemFields::new(&entry.display_name)
.with_icon(harness_display::icon_for(harness));
// Oz is branded locally so stale server/cache values cannot
// reintroduce the legacy product name.
let display_name = if harness == Harness::Oz {
harness_display::display_name(harness)
} else {
&entry.display_name
};
let mut fields =
MenuItemFields::new(display_name).with_icon(harness_display::icon_for(harness));
if let Some(color) = harness_display::brand_color(harness) {
fields = fields.with_override_icon_color(Fill::from(color));
}
@@ -673,10 +703,10 @@ pub fn populate_harness_picker<A: OrchestrationControlAction, V: View>(
// is Unknown but entry.display_name is still correct.
if selected_name.is_none() {
if harness_str.eq_ignore_ascii_case(&initial_harness) {
selected_name = Some(entry.display_name.clone());
selected_name = Some(display_name.to_string());
} else if let Some(target_display) = target_display {
if entry.display_name == target_display {
selected_name = Some(entry.display_name.clone());
if display_name == target_display {
selected_name = Some(display_name.to_string());
}
}
}
@@ -13,9 +13,9 @@ use ai::skills::SkillReference;
use galaxy_core::send_telemetry_from_ctx;
use pathfinder_geometry::vector::vec2f;
use warpui::elements::{
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Empty, Flex,
MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
Stack, Text, Wrap,
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Empty, Flex, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
ParentOffsetBounds, Radius, Stack, Text, Wrap,
};
use warpui::keymap::FixedBinding;
use warpui::{
@@ -128,14 +128,16 @@ impl RunAgentsEditState {
if matches!(req.execution_mode, RunAgentsExecutionMode::Local) {
orch.sanitize_for_local_execution();
}
Self {
let mut state = Self {
orch,
agent_run_configs: req.agent_run_configs.clone(),
base_prompt: req.base_prompt.clone(),
summary: req.summary.clone(),
skills: req.skills.clone(),
plan_id: req.plan_id.clone(),
}
};
state.fill_missing_agent_models();
state
}
pub fn to_request(&self) -> RunAgentsRequest {
@@ -151,6 +153,14 @@ impl RunAgentsEditState {
harness_auth_secret_name: self.orch.auth_secret_name().map(str::to_string),
}
}
fn fill_missing_agent_models(&mut self) {
for config in &mut self.agent_run_configs {
if config.model_id.trim().is_empty() {
config.model_id = self.orch.model_id.clone();
}
}
}
}
impl OrchestrationControlAction for RunAgentsCardViewAction {
@@ -197,6 +207,10 @@ pub enum RunAgentsCardViewAction {
ModelChanged {
model_id: String,
},
AgentModelChanged {
agent_name: String,
model_id: String,
},
HarnessChanged {
harness_type: String,
},
@@ -225,6 +239,8 @@ struct RunAgentsChildState {
conversation_id: Option<AIConversationId>,
removed: bool,
mouse_state: MouseStateHandle,
model_picker:
Option<ViewHandle<crate::view_components::FilterableDropdown<RunAgentsCardViewAction>>>,
}
impl RunAgentsChildState {
@@ -234,6 +250,7 @@ impl RunAgentsChildState {
conversation_id: None,
removed: false,
mouse_state: MouseStateHandle::default(),
model_picker: None,
}
}
}
@@ -401,6 +418,7 @@ fn resolve_interactive_defaults(
}
}
}
state.fill_missing_agent_models();
}
impl RunAgentsCardView {
pub fn new(
@@ -537,6 +555,8 @@ impl RunAgentsCardView {
// to the full confirmation UI.
resolve_interactive_defaults(&mut me.state, &*me.block_model, ctx);
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
me.reconcile_agent_model_selections(ctx);
me.repopulate_agent_model_pickers(ctx);
me.refresh_accept_button_state(ctx);
me.maybe_auto_open_create_modal(ctx);
if let Some(conversation_id) = me.block_model.conversation_id(ctx) {
@@ -548,7 +568,7 @@ impl RunAgentsCardView {
}
});
// Repopulate the model picker when available Warp LLMs change.
// Repopulate the model pickers when available Galaxy LLMs change.
// Only relevant for Oz harness — non-Oz harnesses get their
// model catalog from HarnessAvailabilityModel, not LLMPreferences.
ctx.subscribe_to_model(&LLMPreferences::handle(ctx), |me, _, event, ctx| {
@@ -563,6 +583,8 @@ impl RunAgentsCardView {
ctx,
);
}
me.reconcile_agent_model_selections(ctx);
me.repopulate_agent_model_pickers(ctx);
}
});
@@ -582,6 +604,8 @@ impl RunAgentsCardView {
ctx,
);
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
me.reconcile_agent_model_selections(ctx);
me.repopulate_agent_model_pickers(ctx);
me.refresh_accept_button_state(ctx);
ctx.notify();
}
@@ -593,6 +617,8 @@ impl RunAgentsCardView {
// Deleted events also force a repopulate so this card
// stops surfacing the deleted secret as an option.
oc::repopulate_all_pickers(&mut me.state.orch, &me.handles.pickers, ctx);
me.reconcile_agent_model_selections(ctx);
me.repopulate_agent_model_pickers(ctx);
me.refresh_accept_button_state(ctx);
me.maybe_auto_open_create_modal(ctx);
ctx.notify();
@@ -651,6 +677,7 @@ impl RunAgentsCardView {
};
view.ensure_pickers(ctx);
view.ensure_agent_model_pickers(ctx);
view.refresh_accept_button_state(ctx);
// No-ops if secrets are still in flight; the `AuthSecretsLoaded`
// subscription will retry once they resolve.
@@ -683,6 +710,7 @@ impl RunAgentsCardView {
}
}
}
new_state.fill_missing_agent_models();
// Re-seed an Unset selection from persisted per-harness settings,
// honoring an explicit `Inherit` choice for this harness.
if matches!(
@@ -699,10 +727,14 @@ impl RunAgentsCardView {
|| self.state.orch.execution_mode != new_state.orch.execution_mode;
self.state = new_state;
self.sync_configured_children();
self.ensure_agent_model_pickers(ctx);
self.repopulate_agent_model_pickers(ctx);
if harness_or_model_changed {
// Repopulate pickers and re-arm auto-open for the newly-
// streamed harness.
oc::repopulate_all_pickers(&mut self.state.orch, &self.handles.pickers, ctx);
self.reconcile_agent_model_selections(ctx);
self.repopulate_agent_model_pickers(ctx);
self.has_auto_opened_create_modal = false;
}
self.refresh_accept_button_state(ctx);
@@ -715,6 +747,76 @@ impl RunAgentsCardView {
sync_run_agents_children(&mut self.children, &self.state.agent_run_configs);
}
fn reconcile_agent_model_selections(&mut self, ctx: &mut ViewContext<Self>) {
let harness_type = self.state.orch.harness_type.clone();
let is_local = !self.state.orch.execution_mode.is_remote();
let fallback = if oc::is_model_in_filtered_choices(
&self.state.orch.model_id,
&harness_type,
is_local,
ctx,
) {
self.state.orch.model_id.clone()
} else {
oc::first_filtered_model_id(&harness_type, ctx).unwrap_or_default()
};
for config in &mut self.state.agent_run_configs {
if !oc::is_model_in_filtered_choices(&config.model_id, &harness_type, is_local, ctx) {
config.model_id = fallback.clone();
}
}
}
fn ensure_agent_model_pickers(&mut self, ctx: &mut ViewContext<Self>) {
let appearance = Appearance::as_ref(ctx);
let (styles, _) = oc::picker_styles(appearance);
let harness_type = self.state.orch.harness_type.clone();
let is_local = !self.state.orch.execution_mode.is_remote();
for (config, child) in self.state.agent_run_configs.iter().zip(&mut self.children) {
if child.model_picker.is_some() {
continue;
}
let handle = oc::new_standard_filterable_picker_dropdown(&styles, ctx);
Self::set_upward_filterable_menu_position(&handle, ctx);
let agent_name = config.name.clone();
oc::populate_model_picker_for_harness_with_action(
&handle,
&config.model_id,
&harness_type,
is_local,
move |model_id| RunAgentsCardViewAction::AgentModelChanged {
agent_name: agent_name.clone(),
model_id,
},
ctx,
);
Self::subscribe_filterable_picker_close(&handle, ctx);
child.model_picker = Some(handle);
}
}
fn repopulate_agent_model_pickers(&mut self, ctx: &mut ViewContext<Self>) {
let harness_type = self.state.orch.harness_type.clone();
let is_local = !self.state.orch.execution_mode.is_remote();
for (config, child) in self.state.agent_run_configs.iter().zip(&self.children) {
let Some(handle) = &child.model_picker else {
continue;
};
let agent_name = config.name.clone();
oc::populate_model_picker_for_harness_with_action(
handle,
&config.model_id,
&harness_type,
is_local,
move |model_id| RunAgentsCardViewAction::AgentModelChanged {
agent_name: agent_name.clone(),
model_id,
},
ctx,
);
}
}
fn link_child_conversation(&mut self, agent_name: &str, conversation_id: AIConversationId) {
if !link_run_agents_child(&mut self.children, agent_name, conversation_id) {
log::warn!(
@@ -1207,7 +1309,8 @@ impl View for RunAgentsCardView {
}
let is_blocked = matches!(status, Some(AIActionStatus::Blocked));
let card = render_confirmation_card(&self.state, &self.handles, is_blocked, app);
let card =
render_confirmation_card(&self.state, &self.handles, &self.children, is_blocked, app);
let mut root_stack = Stack::new();
root_stack.add_child(card);
@@ -1292,6 +1395,20 @@ impl TypedActionView for RunAgentsCardView {
self.refresh_accept_button_state(ctx);
ctx.notify();
}
RunAgentsCardViewAction::AgentModelChanged {
agent_name,
model_id,
} => {
if let Some(config) = self
.state
.agent_run_configs
.iter_mut()
.find(|config| config.name == *agent_name)
{
config.model_id = model_id.clone();
}
ctx.notify();
}
RunAgentsCardViewAction::HarnessChanged { harness_type } => {
let block_model = self.block_model.clone();
oc::apply_harness_change(
@@ -1302,6 +1419,8 @@ impl TypedActionView for RunAgentsCardView {
|ctx| block_model.base_model(ctx).map(|id| id.to_string()),
ctx,
);
self.reconcile_agent_model_selections(ctx);
self.repopulate_agent_model_pickers(ctx);
// Harness change resets per-harness selection state, so
// give the new harness a fresh auto-open prompt.
self.has_auto_opened_create_modal = false;
@@ -1441,6 +1560,7 @@ fn diverged_orch_fields_against_config(
fn render_confirmation_card(
state: &RunAgentsEditState,
handles: &RunAgentsCardHandles,
children: &[RunAgentsChildState],
is_blocked: bool,
app: &AppContext,
) -> Box<dyn Element> {
@@ -1448,7 +1568,7 @@ fn render_confirmation_card(
let theme = appearance.theme();
let header = render_header(handles, app);
let body = render_body(state, app);
let body = render_body(state, children, app);
let mut content = Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
@@ -1492,13 +1612,17 @@ fn render_header(handles: &RunAgentsCardHandles, app: &AppContext) -> Box<dyn El
config.render(app)
}
fn render_body(state: &RunAgentsEditState, app: &AppContext) -> Box<dyn Element> {
fn render_body(
state: &RunAgentsEditState,
children: &[RunAgentsChildState],
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
column.add_child(render_summary(state, appearance));
column.add_child(render_agents_section(state, app));
column.add_child(render_agents_section(state, children, app));
Container::new(column.finish())
.with_horizontal_padding(16.)
@@ -1532,7 +1656,11 @@ fn render_summary(state: &RunAgentsEditState, appearance: &Appearance) -> Box<dy
.finish()
}
fn render_agents_section(state: &RunAgentsEditState, app: &AppContext) -> Box<dyn Element> {
fn render_agents_section(
state: &RunAgentsEditState,
children: &[RunAgentsChildState],
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let label = Text::new(
@@ -1543,22 +1671,56 @@ fn render_agents_section(state: &RunAgentsEditState, app: &AppContext) -> Box<dy
.with_color(blended_colors::text_disabled(theme, theme.background()))
.finish();
let pills_row = Wrap::row()
let agent_cards = Wrap::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_spacing(4.)
.with_run_spacing(4.)
.with_spacing(12.)
.with_run_spacing(8.)
.with_children(
state
.agent_run_configs
.iter()
.map(|cfg| render_static_agent_pill(&cfg.name, app)),
.zip(children)
.map(|(config, child)| render_agent_model_card(config, child, app)),
)
.finish();
Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
.with_child(Container::new(label).with_margin_bottom(6.).finish())
.with_child(pills_row)
.with_child(agent_cards)
.finish()
}
fn render_agent_model_card(
config: &RunAgentsAgentRunConfig,
child: &RunAgentsChildState,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let model_label = Text::new(
"Model".to_string(),
appearance.ui_font_family(),
appearance.monospace_font_size() - 1.,
)
.with_color(blended_colors::text_disabled(
appearance.theme(),
appearance.theme().background(),
))
.finish();
let picker = child
.model_picker
.as_ref()
.map(|picker| ChildView::new(picker).finish())
.unwrap_or_else(|| Empty::new().finish());
let content = Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
.with_child(render_static_agent_pill(&config.name, app))
.with_child(Container::new(model_label).with_margin_top(6.).finish())
.with_child(picker)
.finish();
ConstrainedBox::new(content)
.with_width(oc::ORCHESTRATION_PICKER_MAX_WIDTH)
.finish()
}
@@ -1865,7 +2027,6 @@ fn render_editor(
handles: &RunAgentsCardHandles,
app: &AppContext,
) -> Box<dyn Element> {
use warpui::elements::ConstrainedBox;
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut column = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
@@ -1889,11 +2050,24 @@ fn render_editor(
.with_margin_top(12.)
.finish(),
);
column.add_child(oc::render_picker_row(
&state.orch,
&handles.pickers,
appearance,
));
let harness_picker = handles
.pickers
.harness_picker
.as_ref()
.map(|picker| ChildView::new(picker).finish());
column.add_child(
Container::new(
ConstrainedBox::new(oc::render_picker_column(
"Agent harness",
harness_picker,
appearance,
))
.with_width(oc::ORCHESTRATION_PICKER_MAX_WIDTH)
.finish(),
)
.with_margin_top(12.)
.finish(),
);
if let Some(reason) = oc::accept_disabled_reason_with_auth(&state.orch, app) {
column.add_child(oc::render_validation_error(
@@ -37,6 +37,7 @@ fn make_request_with_skills(
name: "child".to_string(),
prompt: "do work".to_string(),
title: "Child agent".to_string(),
model_id: "auto".to_string(),
}],
plan_id: String::new(),
harness_auth_secret_name: None,
@@ -256,6 +257,21 @@ fn to_request_preserves_fields_but_normalizes_execution_to_local() {
assert_eq!(round_tripped.plan_id, req.plan_id);
}
#[test]
fn legacy_child_without_model_inherits_the_run_model() {
let mut request = make_request("oz", RunAgentsExecutionMode::Local);
request.model_id = "fallback-model".to_string();
request.agent_run_configs[0].model_id.clear();
let state = RunAgentsEditState::from_request(&request);
assert_eq!(state.agent_run_configs[0].model_id, "fallback-model");
assert_eq!(
state.to_request().agent_run_configs[0].model_id,
"fallback-model"
);
}
#[test]
fn live_child_links_and_removal_survive_streaming_config_sync() {
let first_id = AIConversationId::new();
@@ -275,11 +291,13 @@ fn live_child_links_and_removal_survive_streaming_config_sync() {
name: "gamma".to_string(),
prompt: "new work".to_string(),
title: String::new(),
model_id: "model-gamma".to_string(),
},
RunAgentsAgentRunConfig {
name: "alpha".to_string(),
prompt: "updated work".to_string(),
title: String::new(),
model_id: "model-alpha".to_string(),
},
];
sync_run_agents_children(&mut children, &configs);