133 lines
12 KiB
Markdown
133 lines
12 KiB
Markdown
# Custom Host Picker — Tech Spec
|
|
|
|
Linear: [QUALITY-701](https://linear.app/warpdotdev/issue/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` — the `HostPicker` view 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` — `OrchestrationPickerHandles` gains a `host_picker` handle; new helpers `populate_host_picker`, `resolve_default_host_slug`, `resolve_recent_host_slug`, and `persist_host_selection`; `sync_picker_selections` is 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-dispatches `WorkerHostChanged`.
|
|
- `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, dispatches `WorkerHostChanged`, 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 dispatches `InternalAction::SelectKnown(slug)`; selecting "Custom host…" dispatches `InternalAction::EnterCustomMode`.
|
|
|
|
- **Custom mode** swaps the dropdown top bar for an inline single-line `EditorView` plus a small cancel button. Enter or blur commits via `commit_custom`; Escape or cancel reverts via `cancel_custom`. The editor is wrapped in a `Flex::column` with `MainAxisAlignment::Center` so 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 outer `Container` with vertical margins equal to `DROPDOWN_PADDING`, mirroring the standard `Dropdown` view'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::Close` is 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 `warp` into custom mode, `commit_custom` collapses back to the standard `warp` selection rather than persisting `warp` as a custom value. This avoids the asymmetric case where `current_slug` is a casing variant of `warp` that 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, calls `picker.set_options(...)`, then `picker.set_selected(initial_host)`. Empty input falls back to `warp`. Used by both card views during `ensure_pickers`.
|
|
|
|
- `resolve_default_host_slug(ctx) -> Option<String>` — returns the workspace default slug, honoring the developer-only `WARP_CLOUD_MODE_DEFAULT_HOST` env var override, otherwise reading from `UserWorkspaces::default_host_slug()`. Mirrors the single-agent ambient flow.
|
|
|
|
- `resolve_recent_host_slug(ctx) -> Option<String>` — returns the persisted last-selected custom slug, deduplicated against `warp` and the workspace default (so the menu doesn't show a duplicate row).
|
|
|
|
- `persist_host_selection(worker_host, ctx)` — writes the slug to `CloudAgentSettings.last_selected_host`. Skipped for empty values and for `warp` so 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 use `set_upward_menu_position` for the same reason). Without this the menu visually collides with the Environment / Base model rows below.
|
|
- Calls `populate_host_picker` to seed options and selection.
|
|
- Subscribes to `HostPickerEvent`: `HostChanged` re-dispatches the existing `RunAgentsCardViewAction::WorkerHostChanged`; `Closed` refocuses 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_picker` to seed options and selection.
|
|
- Subscribes to `HostPickerEvent::HostChanged` to dispatch `OrchestrationConfigBlockAction::WorkerHostChanged`, which updates the edit state, calls `oc::persist_host_selection`, and `apply_field_change` (writes the new value into the plan's stored `OrchestrationConfig`).
|
|
|
|
### 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_items` with no default and no recent → only `warp` + `Custom host…`. (Behavior 4)
|
|
- `build_menu_items` with default set → default row first, badged; then `warp`; then `Custom host…`. (Behavior 4)
|
|
- `build_menu_items` with recent set → `warp` first; then recent as plain slug; then `Custom host…`. (Behavior 4)
|
|
- `build_menu_items` dedups when recent equals default. (Behavior 5)
|
|
- `build_menu_items` dedups when recent equals `warp`. (Behavior 5)
|
|
- `build_menu_items` warp entry dispatches `SelectKnown("warp")`. (Behavior 6)
|
|
- `build_menu_items` custom entry dispatches `EnterCustomMode`. (Behavior 8)
|
|
- `menu_label_for` picks the "Default" badge when the slug matches the workspace default. (Behavior 6)
|
|
- `menu_label_for` returns plain slug for `warp`. (Behavior 6)
|
|
- `menu_label_for` returns plain slug for an unknown value (custom-mode display). (Behavior 15)
|
|
- `normalize_slug` trims whitespace and falls back to `warp` on 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 `defaultHostSlug` configured (toggle via SQL on the local `organization_settings` table, or via `WARP_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-`warp` slug, 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 as `local-dev` is the canonical check; worker logs show `task_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.
|