first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+79
View File
@@ -0,0 +1,79 @@
# PRODUCT — Orchestration Pill Bar
## Summary
When a user is working with an orchestrator agent that has spawned one or more child agents, Warp shows a horizontal "pill bar" above the agent view header listing the orchestrator and each child. Clicking a pill switches the active pane in place to that agent's conversation. When viewing a child agent, the pane title is replaced with a `[Parent] [Child]` breadcrumb path so the user can navigate back to the orchestrator from the same pane.
## Figma
Figma: https://www.figma.com/design/AsF5uAM6L5tUmc11vm9YSi (nodes `4073-19833`, `4073-17179`)
## Goals
- Make the orchestrator → child relationship discoverable from the pane header without opening a separate panel.
- Let the user move between an orchestrator and its children inside a single pane (no implicit splits, no new tabs).
- Keep the existing single-conversation agent view unchanged when no orchestration is in play.
## Non-goals (V1)
- Hover preview popover on a pill (deferred).
- Pin / unpin a child to keep its pane open as a split (deferred).
- 3-dot menu on a pill (Open in new pane / Open in new tab / Stop agent / Kill agent) — deferred.
- Drag-to-reorder pills.
- Any change to non-orchestration conversations.
## Behavior
1. The pill bar only appears in the **fullscreen agent view** (`AgentView` flag on, `agent_view_controller.is_fullscreen()`), and only when the new `OrchestrationPillBar` flag is enabled.
2. The pill bar is shown only when the **active conversation is the orchestrator** — i.e. the conversation that has child agents underneath it. When the user is viewing a child agent, the pill bar is replaced by breadcrumbs in the title (see (10)(14)). When there is no orchestration relationship at all, no pill bar is shown and the pane header renders exactly as before.
3. The pill bar is hidden when the orchestrator has zero children. It only appears once at least one child agent has been spawned.
4. Pill ordering is stable: the orchestrator is always the leftmost pill, followed by child pills in the order the orchestrator registered them (i.e. the order in which they were spawned). Sorting by the first exchange's start time is intentionally avoided because a child whose first exchange has not started yet would otherwise sort to the front and pop into a different position once it began streaming, reshuffling the bar. Pills do **not** reshuffle as their statuses update.
5. Each pill is a horizontal stadium-shaped chip containing:
- A circular avatar (16×16) on the left.
- A label on the right (truncated with an ellipsis past ~110px).
- Internal padding: 4px left of the avatar, 10px right of the label, 6px between avatar and label.
- Pills are 22px tall with a half-stadium corner radius (radius = height/2). Adjacent pills are spaced 6px apart.
6. The orchestrator pill uses the Warp `Oz` glyph on a cyan disc and is labelled with the orchestrator conversation's agent name, falling back to `"Orchestrator"` if no name is set.
7. Each child pill uses:
- A colored disc whose color is deterministic from the agent's name (hash → 6-color palette of `ansi_fg_blue/magenta/cyan/green/yellow/red`).
- The first letter of the agent's name (uppercase), in bold, on top of the disc.
- The agent's name as the label, falling back to `"Agent"` if unset.
Note this is temporary - we'll update this further later.
8. Pill states:
- **Selected** (the pill matches the active conversation): solid foreground background + inverted text color, label rendered in semibold. Cursor is the default arrow. Clicks are no-ops.
- **Hover / active click** (any non-selected pill): a slightly brighter neutral background; cursor becomes the pointing hand.
- **Idle** (non-selected, not hovered): the standard neutral pill background.
9. Clicking a non-selected pill switches the **current pane** to that pill's conversation in place. It does not split the pane or open a new tab. The newly active pill becomes Selected on the next render. After a click on a child pill, the pane header switches from showing the pill bar to showing breadcrumbs (see (10)).
10. While viewing a child agent (the active conversation has a parent), the pane header title area is replaced with a `[Parent] [Child]` breadcrumb path:
- Each crumb is a 24px-tall capsule with a 4px corner radius, 6px horizontal padding, and the same avatar treatment as pills (orchestrator uses Oz glyph + cyan disc; child uses deterministic-color disc + initial letter).
- The separator between crumbs is a `` chevron icon (16×16) in the standard sub-text color.
- The parent crumb's label is the parent conversation's title, falling back to its agent name, and finally to `"Orchestrator"`.
- The trailing (child) crumb is rendered with the brighter "main" text color, no hover, no click.
11. The parent crumb is interactive:
- Hover: applies a neutral hover background and switches to brighter "main" text color; cursor becomes pointing hand.
- Click: navigates the current pane back to the orchestrator. The pane header then switches from breadcrumbs back to showing the pill bar (with the orchestrator pill now Selected).
12. Hover state for both pills and the parent crumb persists across renders. Re-renders triggered by status updates, new exchanges, etc. must not zero out hover state mid-interaction.
13. Long agent names truncate with an ellipsis:
- Pill label: max 110px.
- Crumb label: max 220px.
14. The pill bar's vertical placement does **not** change the pane header title's vertical centering. The pane title and any header buttons (e.g. `ESC for terminal`) remain visually centered within the standard pane header height; the pill bar appears as a separate row below.
15. When the `OrchestrationPillBar` flag is off, none of the above renders. Existing behavior — including the parent-conversation navigation card from the prior orchestration UI — is preserved exactly.
16. The bar redraws when any of the following change for the orchestrator or its children: conversation status, new exchanges, the active conversation, conversation creation, conversation removal/deletion, or entering/exiting the agent view.
17. When entering or exiting the fullscreen agent view, hover state for all pills resets so a stale hover doesn't persist into the next view.
+132
View File
@@ -0,0 +1,132 @@
# TECH — Orchestration Pill Bar
See `PRODUCT.md` in this directory for user-visible behavior. This document covers implementation and validation only.
## Context
The pill bar lives inside the existing pane header chrome rendered for the fullscreen agent view. The header is built in `app/src/terminal/view/pane_impl.rs` (`render_terminal_pane_header`), which composes a 3-column row via `crate::pane_group::pane::view::header::components::render_three_column_header` and then optionally wraps it via `maybe_add_parent_navigation_card`. The wrapped element is returned to `PaneHeader::render` (`app/src/pane_group/pane/view/header/mod.rs`), which constrains the result to `PANE_HEADER_HEIGHT = 34.` for the standard `HeaderContent::Custom` path.
Relevant existing code:
- `app/src/terminal/view/pane_impl.rs (501-556)``maybe_add_parent_navigation_card`, the splice point where the pill bar gets injected below the standard header. Already wraps the header in a `Flex::column` for the pre-existing parent-conversation card.
- `app/src/terminal/view/pane_impl.rs (269-391)``render_header_title`, where the pane title is built. Breadcrumbs short-circuit this when the active conversation has a parent.
- `app/src/pane_group/pane/view/header/components.rs (152-213)``render_three_column_header`. The center column wraps the title in `Align::new(center_row).finish()` so the title vertically centers within the row's stretched height.
- `app/src/pane_group/pane/view/header/mod.rs:52``PANE_HEADER_HEIGHT = 34.` and the `ConstrainedBox::with_height(PANE_HEADER_HEIGHT)` wrap on `HeaderContent::Custom`.
- `app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs` — already exposes `parent_conversation_id` and the existing `parent_conversation_navigation_card` used by the legacy orchestration UI.
- `app/src/ai/blocklist/history_model.rs``BlocklistAIHistoryModel` exposes `child_conversations_of`, `conversation`, and the events the pill bar subscribes to.
- `app/src/terminal/view.rs:25419` — existing handler stub for `TerminalAction::SwitchAgentViewToConversation`, calling `enter_agent_view_for_conversation` to navigate the same pane.
- `crates/warp_features/src/lib.rs``FeatureFlag` enum and `DOGFOOD_FLAGS`.
The feature is gated by a new `FeatureFlag::OrchestrationPillBar`. Existing `Orchestration` and `AgentView` flag behavior is preserved when the new flag is off.
## Proposed changes
### 1. Feature flag
Add `OrchestrationPillBar` to `FeatureFlag` in `crates/warp_features/src/lib.rs:725`. All new code paths gate on `FeatureFlag::OrchestrationPillBar.is_enabled()`.
### 2. New view: `OrchestrationPillBar`
New file `app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs` exposes:
- `pub struct OrchestrationPillBar` — implements `View` with `Entity::Event = ()`.
- Holds `agent_view_controller: ModelHandle<AgentViewController>` and `mouse_states: HashMap<AIConversationId, MouseStateHandle>` for persistent per-pill hover state (per WARP.md's `MouseStateHandle` rule — inline `MouseStateHandle::default()` would silently break clicks).
- Subscribes to `BlocklistAIHistoryModel` for `UpdatedConversationStatus`, `AppendedExchange`, `SetActiveConversation`, `StartedNewConversation`, and to removal events to drop stale mouse states.
- Subscribes to `AgentViewController` for `EnteredAgentView` / `ExitedAgentView` to clear hover state across view transitions.
- A private `pill_specs(&self, app)` helper that:
- Resolves the active conversation, walks up to its orchestrator via `parent_conversation_id`.
- Returns `None` when the active conversation has a parent (child views render breadcrumbs instead) or when the orchestrator has no children.
- Builds an ordered list: orchestrator first, then children sorted by `first_exchange().start_time`.
- `render_pill(spec, mouse_state, app)` — builds a `Hoverable` whose closure rebuilds the pill on each render (selected vs hovered vs idle styling). Click dispatches `PaneHeaderAction::<TerminalAction, TerminalAction>::CustomAction(TerminalAction::SwitchAgentViewToConversation { conversation_id })`. The action wrapper is required because the pill bar lives inside the pane header chrome — `BackingView::handle_custom_action` unwraps it (mirrors `agent_view_back_button`).
- `render_avatar_disc` — renders the colored circle as a `Stack` of (1) a `ConstrainedBox(Container(bg + corner_radius))` and (2) a centered glyph (letter `Text` or `Icon`). The glyph is centered using nested `Flex::column` / `Flex::row` with both `MainAxisAlignment::Center` and `CrossAxisAlignment::Center` on each axis.
Module wiring: `pub mod orchestration_pill_bar;` in `app/src/ai/blocklist/agent_view/mod.rs` and a `pub use orchestration_pill_bar::{render_orchestration_breadcrumbs, OrchestrationPillBar};`.
### 3. Breadcrumb rendering
Same file. `pub fn render_orchestration_breadcrumbs(agent_view_controller, parent_crumb_mouse_state, app) -> Option<Box<dyn Element>>`:
- Returns `None` unless the flag is on, the view is fullscreen, and the active conversation has a parent.
- Builds two `CrumbSpec`s (parent + active child) and wires them into a `Flex::row` with a `ChevronRight` icon separator. Crumbs share the same avatar treatment as pills.
- The parent crumb takes a caller-owned `MouseStateHandle` (must be a field on `TerminalView`, not constructed inline) and dispatches `SwitchAgentViewToConversation` on click. The trailing crumb has no `Hoverable` and no click handler.
We render breadcrumbs manually rather than reusing `crate::ui_components::breadcrumb` because the shared helper does not support a chevron separator or per-crumb avatars.
### 4. New `TerminalAction` variant
`app/src/terminal/view/action.rs`: add `SwitchAgentViewToConversation { conversation_id: AIConversationId }` plus a `Debug` arm. Distinct from `RevealChildAgent` because pill clicks must navigate the current pane in place rather than emit `Event::RevealChildAgent`, which the pane group treats as a request to spawn / reveal a separate pane.
`app/src/terminal/view.rs`: add a handler arm in `handle_action` calling `self.enter_agent_view_for_conversation(None, AgentViewEntryOrigin::ConversationListView, *conversation_id, ctx)`. Add the variant to the `update_agent_view_pane_header`-eligible action list around line 24393.
### 5. `TerminalView` field + construction
`app/src/terminal/view.rs`:
- Add `orchestration_pill_bar: ViewHandle<OrchestrationPillBar>` field on `TerminalView` (next to `agent_view_back_button`, ~2738).
- Construct in `TerminalView::new` alongside `agent_view_controller`. Subscribe to its no-op event so the parent view re-renders when the pill bar notifies (`ctx.subscribe_to_view(&orchestration_pill_bar, |_, _, _, ctx| ctx.notify())`).
### 6. Pane header wiring
`app/src/terminal/view/pane_impl.rs`:
- In `render_header_title`, short-circuit at the top: if `render_orchestration_breadcrumbs(self.agent_view_controller.as_ref(app), self.mouse_states.parent_conversation_header_link.clone(), app)` returns `Some(element)`, return it directly. Returning the element directly (instead of wrapping in `MainAxisSize::Min` Flex) is required: the breadcrumbs row internally uses `Shrinkable` children, and `render_three_column_header` already wraps the title in `Shrinkable + Clipped` which provides a finite main-axis constraint. A `MainAxisSize::Min` wrapper here would forward an infinite constraint and panic the inner `Shrinkable`.
- In `maybe_add_parent_navigation_card`, add an early branch for the new flag:
```rust
if FeatureFlag::OrchestrationPillBar.is_enabled()
&& FeatureFlag::AgentView.is_enabled()
&& self.agent_view_controller.as_ref(app).is_fullscreen()
{
let pinned_header = ConstrainedBox::new(header)
.with_height(PANE_HEADER_HEIGHT)
.finish();
let pill_bar = ChildView::new(&self.orchestration_pill_bar).finish();
return Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
.with_child(pinned_header)
.with_child(pill_bar)
.finish();
}
```
Pinning the header to `PANE_HEADER_HEIGHT` is **load-bearing**, not cosmetic. `Flex::column` passes `max.y = INFINITY` to its non-flex children (`SizeConstraint::child_constraint_along_axis` in `crates/warpui_core/src/presenter.rs:794`). Without the explicit `ConstrainedBox`, the inner `Align` in `render_three_column_header` collapses to the title's small line-box height and the outer row's `CrossAxisAlignment::Stretch` paints children at offset 0 (top) — the title visibly clings to the top of the row instead of being centered. See `crates/warpui_core/src/elements/flex/mod.rs (467-473)` for the cross-axis offset math and `align.rs (77-89)` for Align's infinite-constraint fallback.
### 7. Mouse state wiring
The breadcrumb's parent crumb needs a persistent `MouseStateHandle`. We reuse `TerminalViewMouseStates::parent_conversation_header_link` (already a field on `TerminalView` for the legacy parent-card link), threaded through `render_orchestration_breadcrumbs`. Per-pill mouse state lives on the pill bar view itself (in its `mouse_states` HashMap, ensured/cleared on history events).
## Testing and validation
### Manual / dogfood verification
To verify in a local build, run an orchestrator (e.g. via `/orchestrate`) that spawns at least two child agents and walk through the invariants from `PRODUCT.md`:
- (1)(3): Confirm the pill bar appears only on the orchestrator's view in fullscreen agent mode and disappears when there are zero children or when not in fullscreen.
- (4): Cause status changes on multiple children (commands finishing, in-progress flips). Pill order must not reshuffle.
- (5)(8): Visual check vs Figma (avatar size, pill height, padding, hover/selected styling).
- (9): Click a sibling child pill — the same pane navigates to it (no split spawned, no new tab). Click the orchestrator pill from a child — same.
- (10)(11): On a child view, breadcrumbs replace the title. Click the parent crumb → returns to orchestrator and pill bar reappears.
- (12): Hover a pill, then trigger a re-render (e.g. wait for a status update). Hover state must persist; cursor must remain pointing-hand.
- (14): Compare title vertical centering against the `ESC for terminal` button on the same row. Both must be centered.
- (15): Toggle `OrchestrationPillBar` off in settings. Header must render exactly as before, including the legacy parent-conversation card path.
- (17): Enter and exit the agent view multiple times. No stale hover bleeds through.
### Layout-regression test
Add a unit test next to `OrchestrationPillBar` that lays out the view in a `warpui::App::test` with at least one child conversation, asserting it does not panic. This is the standard "UI components need layout validation tests" requirement from the `create-pr` skill, and it specifically guards the load-bearing `ConstrainedBox::with_height(PANE_HEADER_HEIGHT)` fix in `maybe_add_parent_navigation_card` (see Risks).
### Behavior-driven coverage to consider
- `pill_specs` returning `None` when the active conversation has a parent and when the orchestrator has zero children — pure logic, easy to unit test against a mocked `BlocklistAIHistoryModel`.
- `pill_avatar_color` being deterministic for a given name (idempotency).
- `orchestrator_label` falling back through `agent_name` to `"Orchestrator"`.
## Risks and mitigations
- **Regression: title vertical centering.** The `Flex::column` wrap introduced by this feature inadvertently broke the title's centering until the header was pinned to `PANE_HEADER_HEIGHT`. The pinning is the only reason centering still works — any future refactor of `maybe_add_parent_navigation_card` that loses the `ConstrainedBox::with_height(PANE_HEADER_HEIGHT)` will regress. Add an inline comment at the call site (already done) and the layout-regression test above.
- **Mouse state lifetime.** Constructing `MouseStateHandle::default()` inline at render time silently zeros out hover state every frame. Per-pill state lives in the view's `mouse_states` map; the parent crumb's state is sourced from `TerminalViewMouseStates`. This pattern is enforced by the existing WARP.md guidance.
## Follow-ups
- Hover preview popover on a pill (small thumbnail of the conversation).
- Pin / unpin a child to keep its conversation open in a parallel split.
- 3-dot menu on each pill: `Open in new pane`, `Open in new tab`, `Stop agent`, `Kill agent`.
- Consider extending `crate::ui_components::breadcrumb` to support per-crumb avatars and a chevron separator so the manual breadcrumb rendering here can collapse into the shared helper.