Skip to content

Orchestration pill bar updates: same-pane pills, 3-dot menu, hover card, breadcrumbs#9680

Draft
advait-m wants to merge 25 commits intomasterfrom
advait/orchestration-pills-v2
Draft

Orchestration pill bar updates: same-pane pills, 3-dot menu, hover card, breadcrumbs#9680
advait-m wants to merge 25 commits intomasterfrom
advait/orchestration-pills-v2

Conversation

@advait-m
Copy link
Copy Markdown
Member

Description

Builds out V2 of the orchestration pill bar in the agent view header — same-pane pills with avatars + click-to-switch, 3-dot overflow menu, hover details card with status / cwd / harness / PR / +N -M git diff stats, and split-off-only breadcrumbs that route back to the orchestrator's existing pane/tab when it's already open.

Highlights:

  • Same-pane pills: orchestrator + each child agent rendered as a pill row above the agent view. Clicking an unselected pill switches the active conversation in place via SwitchAgentViewToConversation.
  • 3-dot overflow menu: per-child pill, anchors to the clicked pill's position. Items: Open in new pane, Open in new tab, Stop agent, Kill agent. The menu is a single shared Menu<OrchestrationPillBarAction> rebuilt per-open with the targeted child id.
  • "Open in new tab": actually creates a new tab via a new Event::OpenChildAgentInNewTab plumbed up from TerminalViewpane_group::EventWorkspace, then enters the agent view in the fresh tab.
  • Hover details card: 280-wide card overlay anchored to the hovered pill (300ms hover-in delay). Shows avatar + name, status badge (hidden for orchestrator), cwd, description, harness chip, branch/PR chips from any Artifact::PullRequest, and a new +N -M diff stats chip styled like the prompt git diff stats chip.
  • Diff stats: lazily fetched on hover-in via get_repo_git_summary(cwd) and cached on the pill bar; cache cleared on orchestrator change. Worktree-per-child setups give per-agent stats.
  • Breadcrumbs (split-off panes only): [Parent Avatar] Title › [Child Avatar] Name, wrapped in a horizontal NewScrollable with overlaid scrollbar so a narrow pane can pan and the row stays vertically centered.
  • Breadcrumb parent click: dispatches WorkspaceAction::RestoreOrNavigateToConversation when the orchestrator is already open in another pane/tab/window (focuses it, switching tabs/windows as needed); falls back to SwitchAgentViewToConversation in the current pane otherwise.
  • Cosmetic fixes along the way: clip the pill row to pane bounds (no bleeding into adjacent panes), stable label width independent of hover (siblings don't shift when the dots fade in), only highlight the truly selected pill.

Gated by FeatureFlag::OrchestrationPillBar.

Testing

Tested locally on macOS:

  • Same-pane pill click switches the active child in place; selected pill is the only highlighted one.
  • 3-dot menu opens anchored to the right pill, items dispatch their respective actions, ESC / click-outside closes it cleanly.
  • "Open in new pane" splits right and hosts the child; "Open in new tab" creates a new session tab and enters the child agent view there.
  • Hover card appears after the 300ms delay, repositions when scrubbing across pills, and dismisses with ~80ms hover-out delay. Diff stats chip populates after the async git diff --shortstat completes.
  • Breadcrumbs only render in split-off panes; parent crumb click focuses the existing orchestrator pane (cross-tab + cross-window verified) and falls back to in-place switch when the orchestrator isn't open anywhere.

cargo check -p warp and cargo clippy -p warp --all-targets --tests -- -D warnings both pass.

Server API dependencies

No server API changes.

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Changelog Entries for Stable

CHANGELOG-IMPROVEMENT: Added an orchestration pill bar to the agent view header for navigating between an orchestrator and its child agents, with a hover details card, 3-dot overflow menu, and split-off-pane breadcrumbs.


Conversation: https://staging.warp.dev/conversation/d6c90117-65f0-402d-a125-d6e1d4f0d331
Plan: https://staging.warp.dev/drive/notebook/afADGVQdVqXEGFbnDXVuYR

Co-Authored-By: Oz oz-agent@warp.dev

advait-m and others added 23 commits April 29, 2026 23:34
V1 hid the pill bar from any child agent view (active conv has parent)
and unconditionally rendered breadcrumbs in its place. The new design
keeps the pill bar visible from same-pane child views (so users can
switch sibling -> sibling in place) and reserves breadcrumbs for child
views that have been split off into a separate pane/tab.

Changes:
- Drop the early-return in pill_specs when active conv is a child;
  resolve the orchestrator and build pills for both orchestrator and
  same-pane child views, marking the active conv's pill as Selected
  regardless of which it is.
- Add is_split_off_child(controller, app) helper. Returns true iff the
  active conv has a parent AND the parent is the active conv in a
  different terminal view in this window (per ActiveAgentViewsModel).
- Gate render_orchestration_breadcrumbs on is_split_off_child so
  same-pane child views fall through to pills.
- Short-circuit pill_specs for split-off views (the breadcrumbs in the
  title would otherwise stack with a redundant pill row below).
- Add AgentViewController::terminal_view_id() accessor so the helper
  can compare pane identities.

Co-Authored-By: Oz <oz-agent@warp.dev>
When a child agent has been opened in a different terminal view than
the orchestrator's view (via 'Open in new pane' / 'Open in new tab',
or restored that way), its pill in the orchestrator's pill bar now
swaps the avatar disc for a pin glyph and clicks dispatch
RevealChildAgent (which focuses the existing pane) instead of
SwitchAgentViewToConversation (which navigates in place).

Changes:
- New PillPinState enum on PillSpec; orchestrator pill never carries
  pin state, child pills query ActiveAgentViewsModel to detect a
  different active terminal view than this one.
- render_pill swaps the avatar for an Icon::Pin when pinned. Pinned
  pill clicks dispatch TerminalAction::RevealChildAgent.
- Add Icon::Pin variant + assets/bundled/svg/pin-01.svg (Lucide-style
  pushpin).
- Extend the pane_group RevealChildAgent handler with a fallback that
  focuses an already-visible terminal pane whose terminal view has
  the conversation as its active agent-view conversation. New helper
  PaneGroup::find_visible_terminal_pane_for_conversation walks visible
  terminal panes (skipping hidden-for-close) to perform that lookup.

Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Add the 4 new TerminalAction variants the design's 3-dot overflow menu
will dispatch and route them through TerminalView::handle_action so the
backend behavior is fully wired:

- OpenChildAgentInNewPane / OpenChildAgentInNewTab: emit
  Event::RevealChildAgent (the pane group reveals a hidden child pane
  for the common case and now also focuses an already-visible pane via
  find_visible_terminal_pane_for_conversation, added in Phase B). Tab
  routing for OpenChildAgentInNewTab is a follow-up; for V2-of-V2 both
  paths land on the same handler.
- StopAgentConversation: cancel the ambient task via
  cancel_task_with_toast when the conversation has a task_id; logs a
  TODO for local-conversation cancel.
- KillAgentConversation: cancel the ambient task (if any) and remove
  the conversation from local history. Cloud-side deletion is
  intentionally skipped per V2 non-goals.

The visible 3-dot button + dropdown menu UI is intentionally deferred
to a follow-up phase \u2014 this commit just gets the action surface in
place so the menu can dispatch through it.

Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
Phase B introduced pin detection by querying ActiveAgentViewsModel for
each child agent and comparing the registered terminal view id against
the orchestrator pane's view id. The check fired for every child
because ActiveAgentViewsModel registers the *hidden* child agent
terminal view (created by StartAgentExecutor via
create_hidden_child_agent_conversation), so every child agent always
has a different view id than the orchestrator pane.

Result: every child pill rendered with the pin glyph instead of an
avatar disc, and every click routed through RevealChildAgent rather
than SwitchAgentViewToConversation, breaking the in-place same-pane
switching that Phase A established.

Disable pin detection (force PillPinState::Unpinned for all children)
until pane visibility is properly plumbed into ActiveAgentViewsModel
or PaneGroup. The PillPinState enum, pin glyph rendering path, and
RevealChildAgent dispatch are all kept intact behind that flag so
turning pin detection back on becomes a one-line change.

Co-Authored-By: Oz <oz-agent@warp.dev>
Phase C wired up the four TerminalAction variants (OpenChildAgentInNewPane,
OpenChildAgentInNewTab, StopAgentConversation, KillAgentConversation) but
left the visible UI deferred. This commit adds the menu surface.

Each child pill now renders a trailing 3-dot button (Icon::DotsHorizontal).
Clicking it opens a Menu<OrchestrationPillBarAction> with four items:
"Open in new pane", "Open in new tab", "Stop agent", "Kill agent".
Menu items dispatch the existing Phase C TerminalActions through the
PaneHeaderAction custom-action surface, which TerminalView::handle_action
already handles.

Implementation:

* Added OrchestrationPillBarAction enum (OpenMenu, CloseMenu, plus the
  four menu-item variants, each carrying the target child's
  AIConversationId so a single Menu instance can serve every child).
* Made OrchestrationPillBar a TypedActionView<Action=OrchestrationPillBarAction>;
  changed terminal/view.rs creation site from add_view to
  add_typed_action_view accordingly.
* Added a single Menu<OrchestrationPillBarAction> child view +
  menu_open_for: Option<AIConversationId> state. Items are rebuilt
  per-open with the targeted child's id baked in.
* Subscribed to MenuEvent::Close so click-outside / ESC dismissal
  flows back through CloseMenu.
* Render() now wraps the bar in a Stack with the menu as a positioned
  overlay anchored to BottomLeft when menu_open_for.is_some().
  Per-pill anchoring is a follow-up; current placement lands the menu
  beneath the bar.
* Highlight active-menu pill the same way as is_selected so the user
  can see which pill the open menu is targeting.
* Added separate overflow_button_mouse_states map so the 3-dot button
  has its own hover highlight independent of the pill body, and clean
  it up alongside mouse_states on RemoveConversation /
  EnteredAgentView / ExitedAgentView events. Auto-close the menu if
  the targeted child disappears.

Co-Authored-By: Oz <oz-agent@warp.dev>
The previous overlay used `OffsetPositioning::offset_from_parent` with a
fixed offset from the pill bar's BottomLeft, so every pill's menu opened
in the same place at the far left of the bar regardless of which 3-dot
button the user actually clicked.

Switch to `PositioningAxis::relative_to_stack_child` anchored to a
per-pill saved position id (`overflow_button_position_id(conversation_id)`).
Each child pill's 3-dot button is wrapped in `SavePosition` so its
painted rect is registered in the position cache; when the menu opens,
View::render anchors the menu's top-right corner to the button's
bottom-right corner with a 4px gap (XAxisAnchor::Right -> Right,
YAxisAnchor::Bottom -> Top).

Now the menu opens directly under whichever pill's 3-dot button was
clicked, no matter how far across the bar that pill happens to sit.

Co-Authored-By: Oz <oz-agent@warp.dev>
Phase D from the V2 plan: a per-pill hover details card that surfaces
the agent's name plus its task description, branch, and PR (when any
PullRequest artifact is attached to the conversation).

Mechanics:

* Added `OrchestrationPillBarAction::SetHoveredPill(Option<id>)` and a
  `hovered_pill: Option<AIConversationId>` field on the pill bar.
* Each pill body's Hoverable now opts into a 300ms hover-in delay /
  80ms hover-out delay and dispatches `SetHoveredPill` from its
  `on_hover` handler. The delay matches the standard tooltip cadence so
  scrubbing across the bar doesn't pop a card per pill.
* Wrapped the pill body in `SavePosition` keyed by
  `pill_body_position_id(conversation_id)`. `View::render` uses that
  saved rect (via `relative_to_stack_child`) to anchor the card under
  the hovered pill, mirroring how the 3-dot menu anchors to its own
  saved rect.
* Made the menu and card mutually exclusive at the overlay level: when
  `menu_open_for` is Some, we render the menu and ignore
  `hovered_pill`. `open_menu_for` also clears `hovered_pill` to be
  defensive.

Card content (V1, hide-if-missing):
* avatar disc + bold agent name
* description paragraph (title or initial query, ~200-char truncation)
* branch chip (Icon::GitBranch) and PR chip (Icon::Github + repo#NNNN)
  derived from `Artifact::PullRequest`, when present.

Status badge, harness chip, working-directory line, and diff-stats from
the original spec are deferred until the data they depend on lands on
`AIConversation`/`AmbientAgentTask`.

Co-Authored-By: Oz <oz-agent@warp.dev>
Fills out three of the previously-missing fields from the Figma:

* **Status badge** (Working / Done / Error / Cancelled / Blocked):
  reads `conversation.status()` and reuses the same icon+color mapping
  from `status_icon_and_color` (already used by the conversation
  details panel and the agent run row), so the card and the side
  panel can't drift on what 'Working' looks like.

* **Working directory line**: pulls from
  `AIConversation::initial_working_directory()` with a fallback to
  `current_working_directory()` for ambient agents whose root task
  hasn't yet recorded a CWD. Prefixes with `~/` when the path is
  rooted at `$HOME`.

* **Harness chip**: uses `ai::harness_display` (icon, label, brand
  color) so 'Claude Code' renders with the orange brand color, 'Gemini
  CLI' with blue, etc. Defaults to Warp Agent (Oz) when server
  metadata hasn't loaded yet (in-progress local conversations) so the
  chip slot doesn't pop empty.

Branch chip and PR chip continue to come from
`Artifact::PullRequest`, hidden when no PR is attached.

Still deferred (need new server fields / artifact variants):
  * standalone `Branch` artifact for branches without a PR yet
  * diff stats (`+N -M`) — needs an `Artifact::DiffStats` variant
    or a local git read

Co-Authored-By: Oz <oz-agent@warp.dev>
Two related fixes for the hover details card.

1. **Orchestrator status is misleading**: `conversation.status()` reports
   the conversation's *own* last-exchange status. For an orchestrator
   that has handed off to subagents, that status often ends up as
   `Cancelled` (the user cancelled to delegate) or `Success` (the
   orchestrator's own streaming finished), which doesn't reflect the
   state of the orchestration as a whole. Hiding the badge for
   orchestrator pills (no parent conversation) is the cheapest correct
   fix until we plumb a child-status aggregation accessor.

2. **Status badge overflowing the card** on the orchestrator pill only:
   the header used `MainAxisAlignment::SpaceBetween` with the leading
   group sized `Min` and the name's `max_width` of `HOVER_CARD_WIDTH -
   110`. When the name is long enough to fill its budget, SpaceBetween
   pushes the badge past the right edge of the card instead of
   truncating the name. The orchestrator hits this because its title
   ("Orchestrate Multi-Agent Label Edits") is much longer than child
   names. Fix: cap the badge with a `STATUS_BADGE_MAX_WIDTH = 96`
   ConstrainedBox and compute the name's max_width as the remaining
   horizontal budget so the badge always fits inside the card.

Co-Authored-By: Oz <oz-agent@warp.dev>
Wraps the bar in `Clipped::new(..)` so when the orchestrator's pane is
narrower than the natural width of the pill row (orchestrator + N child
pills), the pills get cut off at the pane boundary instead of painting
over whatever pane sits to the right. The row uses
`MainAxisSize::Min` and the parent agent-view header doesn't enforce a
horizontal bound, so without the explicit clip the trailing pills (and
the bar's own background fill) leak past the pane divider in split
layouts.

Only the bar itself is clipped; the menu / hover-card overlays stay
outside the Clipped wrapper so they can still extend beyond the bar
bounds when anchored to the trailing pills.

Co-Authored-By: Oz <oz-agent@warp.dev>
Previous attempt wrapped the bar in `Clipped::new(..)` to keep pills
inside the pane but the row was still `MainAxisSize::Min`, which
reports the row's *full intrinsic width* as its laid-out size. `Clipped`
clips at its child's reported size, so when the row's intrinsic width
already exceeded the pane width, Clipped's bounds were the same
oversized rect and nothing got cut off.

Switching the row to `MainAxisSize::Max` + `MainAxisAlignment::Start`
makes the row's laid-out width match the parent constraint (the pane
width passed in by the wrapping Flex::column in pane_impl.rs). Children
remain left-packed; any pills past the right edge of the row paint
outside its bounds and get clipped by the surrounding `Clipped` element.

Co-Authored-By: Oz <oz-agent@warp.dev>
Co-Authored-By: Oz <oz-agent@warp.dev>
The dots used to render unconditionally, which made every child pill
read as having an action affordance even at rest. Hide the glyph (and
its hover background) until the pill body is hovered or its menu is
already open.

Slot is still reserved at the same width so neighbouring pills don't
shift on hover \u2014 we just swap the icon for `Empty` when not visible.
Button keeps its mouse handler and `SavePosition` either way; the
menu's anchor stays correct, and clicks on the slot still fire even
when the glyph is invisible.

Co-Authored-By: Oz <oz-agent@warp.dev>
Pill width is now determined by avatar+label alone. The 3-dot overflow
button is rendered as a positioned overlay anchored to the pill's
trailing edge (Stack + add_positioned_child at MiddleRight) and only
shown when the pill is being hovered or its menu is open. Result: no
slot reservation, no width change between rest and hover, no shift of
sibling pills, and the dots visually clip the trailing edge of the
label text rather than pushing it aside.

Co-Authored-By: Oz <oz-agent@warp.dev>
Two fixes for the overlay 3-dot button:

- Shrink the label's max width by the overflow button's footprint when
  show_dots is true so the ellipsis truncates *before* the dots rather
  than running underneath them. At rest the label still gets the full
  budget so the pill keeps its compact width.
- Add with_defer_events_to_children() on the outer pill body Hoverable.
  Previously a click on the 3-dot button fired *both* handlers (open
  menu *and* switch agent view); deferring lets the inner Hoverable
  consume the click so only the menu opens.

Co-Authored-By: Oz <oz-agent@warp.dev>
…ed pill

- Always use the shorter label budget (PILL_LABEL_MAX_WIDTH minus the
  3-dot button footprint) for child pills regardless of hover/menu
  state. Switching the budget on hover caused the pill to *shrink*
  when dots appeared, since Min sizing propagated the smaller width
  outward and shifted siblings. With a fixed budget, child pill widths
  are constant; only the dots overlay appears/disappears.
- Drop menu_is_open_for_this from the 'selected' branch of the
  highlight rule. Opening the 3-dot menu on a non-active pill now
  paints that pill with the regular hover background instead of the
  full selected (foreground/background-inverted) treatment, so only
  the truly active pill reads as selected.

Co-Authored-By: Oz <oz-agent@warp.dev>
Previously, OpenChildAgentInNewTab routed through RevealChildAgent,
which only reveals/focuses the existing hidden child pane within the
orchestrator's pane group \u2014 so picking 'Open in new tab' just opened
a vertical split, not a new tab.

Wire a real new-tab path:
- Add Event::OpenChildAgentInNewTab on TerminalView.
- In TerminalView::handle_action, OpenChildAgentInNewTab emits this
  event instead of RevealChildAgent.
- terminal_pane.rs forwards it as pane_group::Event::OpenChildAgentInNewTab.
- Workspace handles the event by calling
  add_new_session_tab_with_default_mode and then invoking
  enter_agent_view_for_conversation on the new tab's active terminal
  view, switching focus to that tab.

The conversation already lives in BlocklistAIHistoryModel so no
restoration plumbing is needed \u2014 the new terminal view simply enters
agent view for the existing conversation id.

Co-Authored-By: Oz <oz-agent@warp.dev>
In a split-off pane that's been resized down, the orchestration
breadcrumb row (parent avatar/title \u203a child avatar/title) easily
exceeds the title slot's available width and the trailing crumb gets
clipped with no way to read it. Wrap the breadcrumb row in a horizontal\n`NewScrollable` so the user can pan to reveal the clipped portion.

Switch the row to `MainAxisSize::Min` so its intrinsic width is the
sum of its children (rather than always filling the title slot, which
would defeat the scrollable). Persist the scroll handle on
`TerminalViewMouseStates` so scroll position survives renders, and
plumb it into `render_orchestration_breadcrumbs`. Aliased
`warpui::elements::Fill` as `ElementFill` to avoid colliding with the
existing `warp_core::ui::theme::Fill` import.

Co-Authored-By: Oz <oz-agent@warp.dev>
Switch ScrollableAppearance::new(.., overlaid_scrollbar=true) on the
breadcrumb's horizontal scrollbar so it paints on top of the row
instead of reserving a strip below it. Reserving space pushed the
breadcrumbs upward (off-center) when the row overflowed; with the
scrollbar overlaid the row stays vertically centered in the title
slot, with the scrollbar briefly crossing through the bottom of the
labels when scrolling.

Co-Authored-By: Oz <oz-agent@warp.dev>
When the orchestrator is already open in another pane (same tab,
different tab, or different window), clicking the parent breadcrumb
now focuses that existing pane via WorkspaceAction::RestoreOrNavigateToConversation
instead of switching the current pane in place.

Falls back to TerminalAction::SwitchAgentViewToConversation when the
orchestrator isn't open anywhere, so the breadcrumb remains useful
even after the orchestrator's pane has been closed.

Co-Authored-By: Oz <oz-agent@warp.dev>
Each pill's hover card now fetches `git diff --shortstat HEAD` against
the conversation's working directory the first time it's hovered,
caches the result on the OrchestrationPillBar, and renders a chip
matching the styling of the prompt git diff stats chip
(`add_color`/`remove_color` from `code::editor::diff`).

Orchestration child agents typically run in their own git worktrees,
so per-conversation cwd resolves to a per-agent change count. The
cache is cleared whenever the orchestrator changes so stale stats
don't leak across orchestrations.

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label Apr 30, 2026
@advait-m advait-m changed the title Orchestration pill bar V2: same-pane pills, 3-dot menu, hover card, breadcrumbs Orchestration pill bar updates: same-pane pills, 3-dot menu, hover card, breadcrumbs Apr 30, 2026
advait-m and others added 2 commits April 30, 2026 17:56
Child agents in worktrees typically commit their work as they go, so
`git diff --shortstat HEAD` reports 0 immediately after each commit and
the hover card chip never appears. Switch to a new
`get_branch_change_summary` helper that diffs against the detected main
branch (committed-since-fork + uncommitted), giving the cumulative
"diff that would land in a PR" change count per agent. Falls back to
HEAD-relative semantics when on the main branch directly.

Co-Authored-By: Oz <oz-agent@warp.dev>
Branch-vs-main diff inflates the count when a branch was forked from
an older commit (the chip showed +81/-1545 for a +4/-8 actual change).
Pulling the chip out for now \u2014 will re-add with smarter base detection
later (likely merge-base / fork point with cap on the lookback range).

Reverts the OrchestrationPillBar diff_stats fields, async fetch, and
the +N -M chip; also removes the now-unused get_branch_change_summary
helper.

Co-Authored-By: Oz <oz-agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant