12 KiB
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 aFlex::columnfor 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 inAlign::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 theConstrainedBox::with_height(PANE_HEADER_HEIGHT)wrap onHeaderContent::Custom.app/src/ai/blocklist/agent_view/orchestration_conversation_links.rs— already exposesparent_conversation_idand the existingparent_conversation_navigation_cardused by the legacy orchestration UI.app/src/ai/blocklist/history_model.rs—BlocklistAIHistoryModelexposeschild_conversations_of,conversation, and the events the pill bar subscribes to.app/src/terminal/view.rs:25419— existing handler stub forTerminalAction::SwitchAgentViewToConversation, callingenter_agent_view_for_conversationto navigate the same pane.crates/warp_features/src/lib.rs—FeatureFlagenum andDOGFOOD_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— implementsViewwithEntity::Event = ().- Holds
agent_view_controller: ModelHandle<AgentViewController>andmouse_states: HashMap<AIConversationId, MouseStateHandle>for persistent per-pill hover state (per WARP.md'sMouseStateHandlerule — inlineMouseStateHandle::default()would silently break clicks). - Subscribes to
BlocklistAIHistoryModelforUpdatedConversationStatus,AppendedExchange,SetActiveConversation,StartedNewConversation, and to removal events to drop stale mouse states. - Subscribes to
AgentViewControllerforEnteredAgentView/ExitedAgentViewto clear hover state across view transitions.
- Holds
- A private
pill_specs(&self, app)helper that:- Resolves the active conversation, walks up to its orchestrator via
parent_conversation_id. - Returns
Nonewhen 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.
- Resolves the active conversation, walks up to its orchestrator via
render_pill(spec, mouse_state, app)— builds aHoverablewhose closure rebuilds the pill on each render (selected vs hovered vs idle styling). Click dispatchesPaneHeaderAction::<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_actionunwraps it (mirrorsagent_view_back_button).render_avatar_disc— renders the colored circle as aStackof (1) aConstrainedBox(Container(bg + corner_radius))and (2) a centered glyph (letterTextorIcon). The glyph is centered using nestedFlex::column/Flex::rowwith bothMainAxisAlignment::CenterandCrossAxisAlignment::Centeron 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
Noneunless the flag is on, the view is fullscreen, and the active conversation has a parent. - Builds two
CrumbSpecs (parent + active child) and wires them into aFlex::rowwith aChevronRighticon separator. Crumbs share the same avatar treatment as pills. - The parent crumb takes a caller-owned
MouseStateHandle(must be a field onTerminalView, not constructed inline) and dispatchesSwitchAgentViewToConversationon click. The trailing crumb has noHoverableand 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 onTerminalView(next toagent_view_back_button, ~2738). - Construct in
TerminalView::newalongsideagent_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: ifrender_orchestration_breadcrumbs(self.agent_view_controller.as_ref(app), self.mouse_states.parent_conversation_header_link.clone(), app)returnsSome(element), return it directly. Returning the element directly (instead of wrapping inMainAxisSize::MinFlex) is required: the breadcrumbs row internally usesShrinkablechildren, andrender_three_column_headeralready wraps the title inShrinkable + Clippedwhich provides a finite main-axis constraint. AMainAxisSize::Minwrapper here would forward an infinite constraint and panic the innerShrinkable. - In
maybe_add_parent_navigation_card, add an early branch for the new flag:Pinning the header toif 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(); }PANE_HEADER_HEIGHTis load-bearing, not cosmetic.Flex::columnpassesmax.y = INFINITYto its non-flex children (SizeConstraint::child_constraint_along_axisincrates/warpui_core/src/presenter.rs:794). Without the explicitConstrainedBox, the innerAligninrender_three_column_headercollapses to the title's small line-box height and the outer row'sCrossAxisAlignment::Stretchpaints children at offset 0 (top) — the title visibly clings to the top of the row instead of being centered. Seecrates/warpui_core/src/elements/flex/mod.rs (467-473)for the cross-axis offset math andalign.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 terminalbutton on the same row. Both must be centered. - (15): Toggle
OrchestrationPillBaroff 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_specsreturningNonewhen the active conversation has a parent and when the orchestrator has zero children — pure logic, easy to unit test against a mockedBlocklistAIHistoryModel.pill_avatar_colorbeing deterministic for a given name (idempotency).orchestrator_labelfalling back throughagent_nameto"Orchestrator".
Risks and mitigations
- Regression: title vertical centering. The
Flex::columnwrap introduced by this feature inadvertently broke the title's centering until the header was pinned toPANE_HEADER_HEIGHT. The pinning is the only reason centering still works — any future refactor ofmaybe_add_parent_navigation_cardthat loses theConstrainedBox::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'smouse_statesmap; the parent crumb's state is sourced fromTerminalViewMouseStates. 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::breadcrumbto support per-crumb avatars and a chevron separator so the manual breadcrumb rendering here can collapse into the shared helper.