Skip to content
Open
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
43 changes: 30 additions & 13 deletions app/src/ai/blocklist/inline_action/host_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
//! pickers; in custom mode it swaps the top bar for an inline editor that
//! accepts a self-hosted worker slug. The layout mirrors the Oz webapp's
//! host selector: workspace default first (badged "Default"), then warp,
//! then connected worker hosts, then the user's most recent custom slug,
//! then a "Custom host…" entry.
//! then connected worker hosts, then the user's most recent custom slug
//! marked as disconnected if it is not currently connected, then a
//! "Custom host…" entry.

use warp_core::ui::theme::Fill;
use warpui::elements::{
Border, ChildAnchor, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
Expanded, Flex, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement,
PositionedElementAnchor, Radius,
};
use warpui::fonts::{Properties, Weight};
use warpui::platform::Cursor;
use warpui::{
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
Expand Down Expand Up @@ -51,6 +53,8 @@ pub enum HostPickerEvent {

const CUSTOM_HOST_LABEL: &str = "Custom host…";
const DEFAULT_BADGE: &str = "Default";
const CONNECTED_BADGE: &str = "Connected";
const DISCONNECTED_BADGE: &str = "Disconnected";
const EDITOR_PLACEHOLDER: &str = "my-worker-host";

// ── Internal action plumbing ────────────────────────────────────────
Expand Down Expand Up @@ -248,9 +252,9 @@ impl HostPicker {
}

fn sync_dropdown_selection(&mut self, ctx: &mut ViewContext<Self>) {
let label = menu_label_for(&self.current_slug, self.default_host.as_deref());
let action = InternalAction::SelectKnown(self.current_slug.clone());
self.dropdown.update(ctx, |dropdown, ctx_dropdown| {
dropdown.set_selected_by_name(&label, ctx_dropdown);
dropdown.set_selected_by_action(action, ctx_dropdown);
});
}

Expand Down Expand Up @@ -416,8 +420,9 @@ fn normalize_slug(slug: &str) -> String {
}

/// Builds the menu items shown in list mode, in the order: workspace default
/// (badged "Default" if set), warp, connected worker hosts, recent custom
/// slug (if any and not a duplicate), then a "Custom host…" entry.
/// (badged "Default" if set), warp, connected worker hosts (badged
/// "Connected"), recent custom slug (badged "Disconnected" when it is not
/// currently connected), then a "Custom host…" entry.
pub(crate) fn build_menu_items(
default_host: Option<&str>,
recent_host: Option<&str>,
Expand Down Expand Up @@ -451,7 +456,7 @@ pub(crate) fn build_menu_items(
}
items.push(menu_item_for_known(
slug,
None,
Some(CONNECTED_BADGE),
InternalAction::SelectKnown(slug.to_string()),
));
known_slugs.push(slug.to_string());
Expand All @@ -462,10 +467,12 @@ pub(crate) fn build_menu_items(
.any(|known| known.eq_ignore_ascii_case(slug))
{
// Recent hosts render as plain slugs; only the workspace
// default carries a badge.
// default carries a default badge. If the recent host is not in
// the connected set, keep it selectable but make the disconnected
// state explicit.
items.push(menu_item_for_known(
slug,
None,
Some(DISCONNECTED_BADGE),
InternalAction::SelectKnown(slug.to_string()),
));
}
Expand All @@ -481,6 +488,7 @@ pub(crate) fn build_menu_items(

/// Returns the menu label corresponding to `slug`, including the "Default"
/// badge when it matches the workspace default.
#[cfg(test)]
pub(crate) fn menu_label_for(slug: &str, default_host: Option<&str>) -> String {
if default_host == Some(slug) {
format_known_label(slug, Some(DEFAULT_BADGE))
Expand All @@ -489,6 +497,7 @@ pub(crate) fn menu_label_for(slug: &str, default_host: Option<&str>) -> String {
}
}

#[cfg(test)]
fn format_known_label(slug: &str, badge: Option<&str>) -> String {
match badge {
Some(badge) => format!("{slug} ({badge})"),
Expand All @@ -501,10 +510,18 @@ fn menu_item_for_known(
badge: Option<&str>,
action: InternalAction,
) -> MenuItem<DropdownAction> {
MenuItem::Item(
MenuItemFields::new(format_known_label(slug, badge))
.with_on_select_action(DropdownAction::select_action_and_close(action)),
)
let mut fields = MenuItemFields::new(slug)
.with_on_select_action(DropdownAction::select_action_and_close(action));
if let Some(badge) = badge {
fields = fields.with_right_side_label(
badge,
Properties {
weight: Weight::Semibold,
..Default::default()
},
);
}
MenuItem::Item(fields)
}

// ── Entity / View impls ─────────────────────────────────────────────
Expand Down
6 changes: 3 additions & 3 deletions app/src/ai/blocklist/inline_action/host_picker_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fn build_menu_items_promotes_default_to_top() {
// matching the Oz webapp's HostSelector layout.
let items = build_menu_items(Some("my-corp"), None, &[]);
assert_eq!(items.len(), 3);
assert_eq!(item_label(&items[0]), "my-corp (Default)");
assert_eq!(item_label(&items[0]), "my-corp");
assert_eq!(item_label(&items[1]), ORCHESTRATION_WARP_WORKER_HOST);
assert_eq!(item_label(&items[2]), "Custom host\u{2026}");
}
Expand All @@ -61,7 +61,7 @@ fn build_menu_items_dedups_recent_when_it_matches_default_or_warp() {
// Same as the workspace default → no duplicate "Recent" row.
let items = build_menu_items(Some("my-corp"), Some("my-corp"), &[]);
assert_eq!(items.len(), 3);
assert_eq!(item_label(&items[0]), "my-corp (Default)");
assert_eq!(item_label(&items[0]), "my-corp");
assert_eq!(item_label(&items[1]), ORCHESTRATION_WARP_WORKER_HOST);
assert_eq!(item_label(&items[2]), "Custom host\u{2026}");

Expand All @@ -80,7 +80,7 @@ fn build_menu_items_adds_connected_hosts_before_recent_and_dedups_known_hosts()
];
let items = build_menu_items(Some("my-corp"), Some("beta"), &connected_hosts);
assert_eq!(items.len(), 5);
assert_eq!(item_label(&items[0]), "my-corp (Default)");
assert_eq!(item_label(&items[0]), "my-corp");
assert_eq!(item_label(&items[1]), ORCHESTRATION_WARP_WORKER_HOST);
assert_eq!(item_label(&items[2]), "alpha");
assert_eq!(item_label(&items[3]), "beta");
Expand Down
5 changes: 0 additions & 5 deletions app/src/ai/blocklist/inline_action/orchestration_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,11 +928,6 @@ pub fn populate_host_picker<V: View>(
};
let mut connected_hosts = ConnectedSelfHostedWorkersModel::as_ref(ctx)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Removing the initial host from the menu source means an existing remote config/request whose worker_host is not currently connected and is not the persisted recent host is treated as unknown by set_selected, which opens the custom editor instead of showing the disconnected row. Keep initial in the item list as a disconnected option so existing selections remain visible and editable.

.worker_hosts_excluding(default_host.as_deref());
if !initial.eq_ignore_ascii_case(ORCHESTRATION_WARP_WORKER_HOST)
&& default_host.as_deref() != Some(initial.as_str())
{
connected_hosts.push(initial.clone());
}
connected_hosts.sort();
connected_hosts.dedup();
picker.update(ctx, |picker, picker_ctx| {
Expand Down
58 changes: 41 additions & 17 deletions app/src/terminal/view/ambient_agent/host_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ const BUTTON_TOOLTIP: &str = "Execution host";

const MENU_HEADER_LABEL: &str = "Execution host";

const DEFAULT_BADGE: &str = "Default";

const CONNECTED_BADGE: &str = "Connected";

const DISCONNECTED_BADGE: &str = "Disconnected";

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Host {
Warp,
Expand Down Expand Up @@ -286,22 +292,30 @@ fn build_menu_items(
right_side_fields: None,
};

let item_for = |host: Host| {
let item_for = |host: Host, badge: Option<&str>| {
let label = host.display_name().to_string();
MenuItem::Item(
MenuItemFields::new(label)
.with_font_size_override(ITEM_FONT_SIZE)
.with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING)
.with_override_hover_background_color(hover_background)
.with_on_select_action(HostSelectorAction::SelectHost(host)),
)
let mut fields = MenuItemFields::new(label)
.with_font_size_override(ITEM_FONT_SIZE)
.with_padding_override(ITEM_VERTICAL_PADDING, MENU_HORIZONTAL_PADDING)
.with_override_hover_background_color(hover_background)
.with_on_select_action(HostSelectorAction::SelectHost(host));
if let Some(badge) = badge {
fields = fields.with_right_side_label(
badge,
Properties {
weight: Weight::Semibold,
..Default::default()
},
);
}
MenuItem::Item(fields)
};

let mut items = vec![header];
if let Some(host) = default_host {
items.push(item_for(host.clone()));
items.push(item_for(host.clone(), Some(DEFAULT_BADGE)));
}
items.push(item_for(Host::Warp));
items.push(item_for(Host::Warp, None));
let default_slug = match default_host {
Some(Host::SelfHosted { slug }) => Some(slug.as_str()),
Some(Host::Warp) | None => None,
Expand All @@ -310,15 +324,25 @@ fn build_menu_items(
.worker_hosts_excluding(default_slug)
.into_iter()
.collect::<Vec<_>>();
if let Host::SelfHosted { slug } = selected {
if default_slug != Some(slug.as_str()) {
connected_hosts.push(slug.clone());
}
}
connected_hosts.sort();
connected_hosts.dedup();
for host in connected_hosts {
items.push(item_for(Host::SelfHosted { slug: host }));
for host in &connected_hosts {
items.push(item_for(
Host::SelfHosted { slug: host.clone() },
Some(CONNECTED_BADGE),
));
}
if let Host::SelfHosted { slug } = selected {
let is_default = default_slug == Some(slug.as_str());
let is_connected = connected_hosts
.iter()
.any(|host| host.eq_ignore_ascii_case(slug));
if !is_default && !is_connected {
items.push(item_for(
Host::SelfHosted { slug: slug.clone() },
Some(DISCONNECTED_BADGE),
));
}
}
items
}
Expand Down
Loading