12 KiB
Custom Host Picker — Tech Spec
Linear: QUALITY-701
Companion product spec: specs/QUALITY-701/PRODUCT.md
Context
The orchestration UI (orchestrate confirmation card and plan-card orchestration block) hosts a row of pickers — model, harness, environment — that drive child agent dispatch. Until now there was no UI to choose the worker host: the dispatched RunAgents request always carried worker_host = "warp", which routes to the default Warp cluster. Customers running self-hosted workers had no way to target them from the desktop client; the Oz webapp's host selector is the only existing entry point.
The picker chrome used by the other orchestration pickers is built around the standard Dropdown view in app/src/view_components/dropdown.rs, styled via picker_styles() in orchestration_controls.rs. Both card views construct their picker handles via shared helpers in orchestration_controls.rs and store them in OrchestrationPickerHandles. The worker-host slug already flows end-to-end as a field on OrchestrationEditState and on RunAgentsExecutionMode::Remote; the missing piece is the UI control that lets a user change it.
The workspace default slug is already exposed to the client as defaultHostSlug on AmbientAgentSettings and surfaced via UserWorkspaces::default_host_slug(). There is also persisted per-user "last selected host" state in CloudAgentSettings.last_selected_host. The Oz webapp's HostSelector (client/packages/agents/src/components/HostSelector.tsx) is the canonical reference for option ordering, default-host preselection, and recent-host surfacing.
Relevant files
New
app/src/ai/blocklist/inline_action/host_picker.rs— theHostPickerview itself (list mode + custom mode).app/src/ai/blocklist/inline_action/host_picker_tests.rs— unit tests for the pure helpers.
Shared picker plumbing (modified)
app/src/ai/blocklist/inline_action/orchestration_controls.rs—OrchestrationPickerHandlesgains ahost_pickerhandle; new helperspopulate_host_picker,resolve_default_host_slug,resolve_recent_host_slug, andpersist_host_selection;sync_picker_selectionsis taught to drive the host picker.app/src/ai/blocklist/inline_action/mod.rs— registers the new module.
Call sites (modified)
app/src/ai/blocklist/inline_action/run_agents_card_view.rs— confirmation card: builds the picker, opens its menu upward, subscribes to its events, re-dispatchesWorkerHostChanged.app/src/ai/document/orchestration_config_block.rs— plan card: builds the picker, opts the inner menu into the overlay layer, subscribes to its events, dispatchesWorkerHostChanged, persists field changes.
Reference
client/packages/agents/src/components/HostSelector.tsx(warp-server) — the webapp's host selector.
Proposed changes
1. New HostPicker view
A single non-generic view that internally switches between two render modes:
-
List mode wraps an inner
Dropdown<InternalAction>styled identically to the other orchestration pickers (picker_styles()). The menu is populated with the workspace default (badged "Default"),warp, the most-recent custom slug, and a "Custom host…" entry, in that order. Selecting any known item dispatchesInternalAction::SelectKnown(slug); selecting "Custom host…" dispatchesInternalAction::EnterCustomMode. -
Custom mode swaps the dropdown top bar for an inline single-line
EditorViewplus a small cancel button. Enter or blur commits viacommit_custom; Escape or cancel reverts viacancel_custom. The editor is wrapped in aFlex::columnwithMainAxisAlignment::Centerso the glyphs sit at the vertical center of the picker box (otherwise the row's tight cross-axis constraint forces the editor to fill the height and the text renders flush to the top). The custom-mode container is wrapped in an outerContainerwith vertical margins equal toDROPDOWN_PADDING, mirroring the standardDropdownview's outer wrapping so the custom box sits at the same y offset as the other pickers in the row.
The picker emits two public events:
HostPickerEvent::HostChanged { slug }— sent whenever the current selection changes.HostPickerEvent::Closed— sent whenever the menu closes or the editor blurs, so the parent can refocus its own input.
Public API:
set_options(default_host, recent_host, ctx)— replaces the menu rows.set_selected(slug, ctx)— sets the displayed slug; unknown slugs switch into custom mode pre-filled with the slug.set_use_overlay_layer(bool, ctx)— forwarded to the inner dropdown.set_menu_position(element_anchor, child_anchor, ctx)— forwarded to the inner dropdown.
Two subtleties worth noting in the implementation:
- The inner dropdown's
DropdownEvent::Closeis suppressed while the picker is transitioning into custom mode. If we let it through, the parent card refocuses itself, blurs the editor we just focused, and the resulting commit-on-blur immediately reverts custom mode — making "Custom host…" feel like a no-op. - When the user types
warpinto custom mode,commit_customcollapses back to the standardwarpselection rather than persistingwarpas a custom value. This avoids the asymmetric case wherecurrent_slugis a casing variant ofwarpthat doesn't match any menu label.
Pure helpers (build_menu_items, menu_label_for, normalize_slug) live at the bottom of the module and are unit-tested without spinning up a view context.
2. Shared orchestration helpers
OrchestrationPickerHandles gets a new host_picker: Option<ViewHandle<HostPicker>> field. sync_picker_selections is taught to call picker.set_selected(...) with the current worker_host whenever the edit state changes; this handles both initial population and subsequent changes from other pickers (e.g. mode toggle resetting host to warp).
Four new free functions in orchestration_controls.rs:
-
populate_host_picker(picker, initial_host, ctx)— reads the workspace default and recent slug, callspicker.set_options(...), thenpicker.set_selected(initial_host). Empty input falls back towarp. Used by both card views duringensure_pickers. -
resolve_default_host_slug(ctx) -> Option<String>— returns the workspace default slug, honoring the developer-onlyWARP_CLOUD_MODE_DEFAULT_HOSTenv var override, otherwise reading fromUserWorkspaces::default_host_slug(). Mirrors the single-agent ambient flow. -
resolve_recent_host_slug(ctx) -> Option<String>— returns the persisted last-selected custom slug, deduplicated againstwarpand the workspace default (so the menu doesn't show a duplicate row). -
persist_host_selection(worker_host, ctx)— writes the slug toCloudAgentSettings.last_selected_host. Skipped for empty values and forwarpso those never become "recent" entries.
Both card views also pre-fill defaults when restoring a Remote config with an empty host: prefer the workspace default over the bare warp fallback so self-hosted teams see their default pre-selected, matching the Oz webapp.
3. Confirmation card wiring (run_agents_card_view.rs)
ensure_pickers constructs a HostPicker for the new host_picker slot and:
- Calls
picker.set_menu_position(TopLeft, BottomLeft)so the open menu flips upward, matching the other dropdowns in this card (which useset_upward_menu_positionfor the same reason). Without this the menu visually collides with the Environment / Base model rows below. - Calls
populate_host_pickerto seed options and selection. - Subscribes to
HostPickerEvent:HostChangedre-dispatches the existingRunAgentsCardViewAction::WorkerHostChanged;Closedrefocuses the card.
The existing WorkerHostChanged handler updates state.orch.worker_host and calls oc::persist_host_selection, so any path that ends in a host change persists the slug.
4. Plan-card wiring (orchestration_config_block.rs)
ensure_pickers constructs a HostPicker for the new host_picker slot and:
- Calls
picker.set_use_overlay_layer(true)so the menu paints above siblings, matching the other pickers in this view (which all opt into the overlay layer). - Calls
populate_host_pickerto seed options and selection. - Subscribes to
HostPickerEvent::HostChangedto dispatchOrchestrationConfigBlockAction::WorkerHostChanged, which updates the edit state, callsoc::persist_host_selection, andapply_field_change(writes the new value into the plan's storedOrchestrationConfig).
5. No other call sites
The worker_host field already exists on OrchestrationEditState and on RunAgentsExecutionMode::Remote, so no downstream changes (dispatch, server marshalling, auto-launch matching) are needed. The previously-hardcoded "warp" value flows through the same code paths as any user-selected slug.
Testing and validation
Unit tests (host_picker_tests.rs)
The pure helpers are tested directly without a view context. Covers product invariants 4, 5, 11, 14, 16, 18.
build_menu_itemswith no default and no recent → onlywarp+Custom host…. (Behavior 4)build_menu_itemswith default set → default row first, badged; thenwarp; thenCustom host…. (Behavior 4)build_menu_itemswith recent set →warpfirst; then recent as plain slug; thenCustom host…. (Behavior 4)build_menu_itemsdedups when recent equals default. (Behavior 5)build_menu_itemsdedups when recent equalswarp. (Behavior 5)build_menu_itemswarp entry dispatchesSelectKnown("warp"). (Behavior 6)build_menu_itemscustom entry dispatchesEnterCustomMode. (Behavior 8)menu_label_forpicks the "Default" badge when the slug matches the workspace default. (Behavior 6)menu_label_forreturns plain slug forwarp. (Behavior 6)menu_label_forreturns plain slug for an unknown value (custom-mode display). (Behavior 15)normalize_slugtrims whitespace and falls back towarpon empty input. (Behavior 14)
Manual validation
The view-driven behaviors (custom-mode commit, blur, focus return, layer interaction with sibling pickers) are covered by manual smoke testing rather than view-level tests:
- Behavior 1, 2, 3: Open a plan with orchestration approved and an orchestrate confirmation card; verify the host picker is present in both surfaces, only in Cloud mode, and visually matches the model / harness / environment pickers.
- Behavior 4, 5, 6: With and without
defaultHostSlugconfigured (toggle via SQL on the localorganization_settingstable, or viaWARP_CLOUD_MODE_DEFAULT_HOST), verify the dropdown contents and ordering, the "Default" badge, and the badge appearing in the collapsed top bar. - Behavior 8, 9, 10, 11, 12: Open custom mode, verify the editor is pre-filled and focused; type a slug, press Enter; reopen the menu and verify the slug now appears as a recent entry. Repeat with Escape and with the cancel button. Try committing an empty buffer and the literal string
warp/WARP. - Behavior 13: Visually compare the custom-mode box to its neighbours; the editor text should be vertically centered and the box should sit at the same y as the sibling pickers.
- Behavior 15: Use the developer override (
WARP_CLOUD_MODE_DEFAULT_HOST=some-unknown-slug) and verify the picker boots into custom mode pre-filled with the slug. - Behavior 16, 17, 18: Pick a custom slug, dismiss the card, then reopen another plan / confirmation card; verify the slug appears as the recent entry. With a workspace default set, verify the recent entry deduplicates against it.
- Behavior 19: Pick a non-
warpslug, dispatch the agents, and verify the worker-host slug reaches the worker. End-to-end smoke against a local Oz stack with a self-hosted worker registered aslocal-devis the canonical check; worker logs showtask_claimed worker_id:"local-dev"when the custom slug is routed correctly. - Behavior 20: Open the menu in each surface and confirm it doesn't visually overlap the Environment or Base model rows.
- Behavior 21: After every menu close or custom-mode commit, the input box of the parent card should regain focus.
Presubmit
cargo fmt, cargo clippy --workspace --all-targets --all-features --tests -- -D warnings, and the host_picker nextest suite all pass clean. Run ./script/presubmit before opening the PR.
Parallelization
Not used. The implementation is a single new view file plus thin wiring at two call sites; the work is sequentially tightly coupled (helpers feed the view, the view feeds both call sites) and small enough that splitting across agents would add coordination overhead without saving wall-clock time.