Skip to content

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

Merged
advait-m merged 39 commits intomasterfrom
advait/orchestration-pills-v2
May 5, 2026
Merged

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

Conversation

@advait-m
Copy link
Copy Markdown
Member

@advait-m advait-m commented Apr 30, 2026

Demo Loom: https://www.loom.com/share/69d68745f1384c0e8c0a96602f8c1a12
Focus panes demo: https://www.loom.com/share/2c07193874df49ce868c6ca04ee5a990
Fixes https://linear.app/warpdotdev/issue/QUALITY-590/pill-bar-updates-v2

Removes old UI i.e. rows for each agent pinned at bottom of convo (we remove only if pills FF is on) and adds the pills FF to dogfood flags!

Description

Builds out V2 of the orchestration pill bar in the agent view header — same-pane pills with avatars + click-to-switch, a 3-dot overflow menu, hover details card, split-off-only breadcrumbs that route back to the orchestrator's existing pane/tab, and an architectural "single source of truth" for which terminal view owns each child agent conversation.

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. When the conversation is already open in another visible pane (this tab, another tab, or another window), the click instead focuses that pane via the same FocusOpenedConversation action the menu uses.
  • 3-dot overflow menu: per-child pill, anchors to the clicked pill's position. Items are context-dependent:
    • When the conversation is not open elsewhere: Open in new pane, Open in new tab.
    • When the conversation is open in another visible pane: a single Focus pane item that focuses the existing pane.
    • Stop agent / Kill agent items are intentionally hidden for now (the action wiring stays so they can be re-added once the underlying behaviour is reliable).
  • "Open in new tab": actually creates a new tab via Event::OpenChildAgentInNewTab plumbed up from TerminalViewpane_group::EventWorkspace, then enters the agent view in the fresh tab.
  • "Open in new pane": splits a fresh terminal pane to the right and goes through the cloud load+restore path so the new pane shows the full transcript (instead of revealing the empty hidden child pane created at agent start).
  • 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, and branch/PR chips from any Artifact::PullRequest.
  • 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.
  • Focus path picker (pill clicks, menu's "Focus pane", and the breadcrumb parent crumb) all share the same logic: resolve the canonical owner from BlocklistAIHistoryModel, then dispatch TerminalAction::RevealChildAgent when the owner pane is in the same pane group (sibling pane in this tab) or WorkspaceAction::FocusTerminalViewInWorkspace when it's in a different pane group (other tab / window). Going through the workspace's focus_pane from a foreign ViewContext doesn't reliably move focus to siblings, hence the split.
  • Single source of truth for child conversations: a new BlocklistAIHistoryEvent::ConversationOwnershipTransferred event is emitted by set_active_conversation_id whenever a conversation moves from one terminal view's live list to another. TerminalView::handle_ai_history_model_event listens for it and drops stale AIBlock / AgentViewEntry rich content, so the previous owner's pane goes blank and the new owner is the only place that renders the transcript. Closing a split-off pane transfers its child conversations back to the parent's owning view.
  • 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, vertical 3-dot glyph rendered as a positioned overlay on hover.

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. The menu collapses to a single Focus pane item when the conversation is already open in another visible pane.
  • Pill click and menu's "Focus pane" both route through the same handler, so they behave identically. Verified focus shift works for sibling panes in the same tab, panes in another tab, and panes in another window.
  • "Open in new pane" splits right and hosts the child with a populated transcript; "Open in new tab" creates a new session tab and enters the child agent view there. The orchestrator pane reverts to the parent conversation when its currently-active child gets split off.
  • Closing a split-off pane transfers any child conversations back to the orchestrator pane; the orchestrator's pill bar continues to function and can re-open the children.
  • Hover card appears after the 300ms delay, repositions when scrubbing across pills, and dismisses with ~80ms hover-out delay.
  • Breadcrumbs only render in split-off panes; parent crumb click focuses the existing orchestrator pane (cross-tab + cross-window + same-tab sibling pane verified) and falls back to in-place switch when the orchestrator isn't open anywhere.

cargo check -p warp, cargo fmt, and cargo clippy --workspace --all-targets --all-features --tests -- -D warnings all pass.

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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Co-Authored-By: Oz <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
…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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
@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 5 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 <[email protected]>
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 <[email protected]>
Previously "Open in new pane" routed through `Event::RevealChildAgent`,
which un-hides the orchestrator's hidden child pane. That pane's
terminal model never accumulated rendered AI blocks for the
conversation \u2014 those got inserted into whichever pane was last hosting
the in-place agent view via `SwitchAgentViewToConversation` \u2014 so
revealing it showed a blank transcript.

Switch "Open in new pane" to mirror "Open in new tab": split a fresh
terminal pane to the right and call `enter_agent_view_for_conversation`
on it. With `is_live` false for the fresh view, that goes through the
cloud load+restore path and populates the full history.

The hidden pane is left untouched. `RevealChildAgent` (used by the
legacy child-agent status card and the pinned-pill flow) is unchanged
\u2014 those paths have the same blank-transcript issue but are deferred
for a separate fix.

Co-Authored-By: Oz <[email protected]>
The server rejects cancel requests for terminated agent runs with
"Terminated agent runs cannot be cancelled", which surfaced as an
error toast every time a user clicked Stop or Kill on a finished
agent. Gate the cancel attempt on the conversation's status being
`InProgress` so we no-op for already-finished runs.

Kill still removes the conversation from local history regardless of
whether the cancel was attempted.

Co-Authored-By: Oz <[email protected]>
When a child agent conversation moved between terminal views, the
rendered AI blocks were left in the previous owner's view while new
exchanges streamed to the new owner — producing a transcript split
across two panes. Fix this by treating the conversation as having a
single canonical owner at all times and proactively cleaning up stale
state when ownership transfers.

- Add `BlocklistAIHistoryEvent::ConversationOwnershipTransferred`,
  emitted from `set_active_conversation_id` for each previous owner
  whose live list contained the conversation. `TerminalView` listens
  for this event on the previous-owner side and drops any AI block /
  agent-view-entry rich content tagged to that conversation, so the
  new owner is the sole renderer.
- After 'Open in new pane' / 'Open in new tab' from the orchestration
  pill bar's 3-dot menu, silently revert the source view to the
  parent orchestrator so its in-place pill click keeps working.
- When a split-off pane closes, transfer ownership of any child agent
  conversations back to whichever pane owns the parent so the
  orchestrator's pill bar continues to function in place.
- 3-dot menu: collapse 'Open in new pane' / 'Open in new tab' into a
  single 'Focus pane' item when the conversation is already open in
  another pane/tab. The action routes to
  `WorkspaceAction::RestoreOrNavigateToConversation`, mirroring the
  breadcrumb parent-click path.
- Pill click: if the conversation is open in a different terminal
  view, dispatch `RestoreOrNavigateToConversation` so the click
  focuses the existing pane instead of switching the orchestrator
  pane in place. Falls back to the original switch-in-place behavior
  when not open elsewhere.

Co-Authored-By: Oz <[email protected]>
advait-m and others added 7 commits May 4, 2026 17:48
The 'Focus pane' label was appearing for every child agent's 3-dot
menu (and every pill click was routing to RestoreOrNavigateToConversation)
because we were using ActiveAgentViewsModel::terminal_view_id_for_conversation
to detect cross-pane ownership. That model tracks every controller whose
active_conversation_id matches the conversation, including the hidden
child-agent pane created by create_hidden_child_agent_conversation, so
it always reported a non-self owner for unentered children.

Replace with a new helper that uses BlocklistAIHistoryModel's canonical
owner (which is mutated by ConversationOwnershipTransferred) and then
verifies the owning pane appears in PaneGroup::visible_pane_ids() across
WorkspaceRegistry. visible_pane_ids() excludes hidden-for-child-agent /
hidden-for-close panes, so the pill bar now only collapses to 'Focus
pane' when the conversation truly lives in a different visible pane.

Co-Authored-By: Oz <[email protected]>
The 'Focus pane' menu item (and the equivalent pill click when the
conversation lives in another visible pane) was routing through
WorkspaceAction::RestoreOrNavigateToConversation, which in turn calls
set_active_conversation_id on whatever terminal_view_id was passed in.
We were sourcing that id from AgentConversationsModel::nav_data, which
can be stale after split-off operations and frequently still pointed
at the orchestrator pane. The result: clicking 'Focus pane' yanked the
conversation into the orchestrator pane and blanked the original
owner pane.

Replace both call sites with WorkspaceAction::FocusTerminalViewInWorkspace,
which only shifts focus and never mutates ownership. Resolve the
canonical owner directly from BlocklistAIHistoryModel (the single
source of truth for which terminal view renders a conversation's AI
blocks), so the focus target always matches whichever pane currently
displays the transcript.

Co-Authored-By: Oz <[email protected]>
The pill-click handler tried to dispatch WorkspaceAction directly from
the Hoverable's on_click closure, which apparently doesn't reach the
workspace handler the way a typed action dispatched from a
ViewContext<Self> does (the menu-driven Focus pane path works because
it goes through the pill bar's handle_action, which dispatches the
WorkspaceAction from its own ViewContext).

Route the pill click's 'open elsewhere' branch through
OrchestrationPillBarAction::FocusOpenedConversation instead. The pill
bar's handle_action then funnels both entry points (3-dot menu item +
pill click) through the same code path, so they behave identically
and the WorkspaceAction is dispatched from the same context that
already worked.

Co-Authored-By: Oz <[email protected]>
When the canonical owner pane lives in the same pane group as the
orchestrator (e.g. 'Open in new pane' was used to split the child
into a sibling pane in the same tab), dispatching
WorkspaceAction::FocusTerminalViewInWorkspace via the workspace's
focus_pane was not actually shifting focus to the sibling pane on
mouse events from the pill bar. The pane group's RevealChildAgent
event handler already calls group.focus_pane(.., true, ctx) from its
own ViewContext<PaneGroup>, which works reliably for sibling panes,
and it has a fallback that handles the case where the conversation
lives in an already-visible pane (not just the hidden child pane).

Pick the focus path based on whether the canonical owner is in the
same pane group as us:
  * Same pane group -> dispatch TerminalAction::RevealChildAgent
    (same path as the pin pill click).
  * Different pane group (other tab / window) -> dispatch
    WorkspaceAction::FocusTerminalViewInWorkspace (which activates
    the containing tab and walks across windows).

Both menu 'Focus pane' and pill click go through the same
FocusOpenedConversation handler, so they continue to behave
identically regardless of where the target pane lives.

Co-Authored-By: Oz <[email protected]>
The 'Open in new pane' / 'Open in new tab' flow does NOT remove the
conversation's entry from PaneGroup::child_agent_panes (the map of
hidden child panes registered at agent start). It just creates a new
pane and migrates the conversation's AI blocks to it. So after a
split, both exist:
  * the original hidden child pane (empty terminal model, no rendered
    AI blocks for the conversation)
  * the new visible pane that hosts the migrated transcript

The previous RevealChildAgent handler checked child_agent_panes first
and only fell back to the visible-pane lookup when no hidden entry
existed, so 'Focus pane' on a split-off child re-revealed the empty
hidden pane next to the real one — the user saw a 'new pane' open
with no content.

Flip the check order: look up the visible owner pane first via
find_visible_terminal_pane_for_conversation and focus it directly.
Only fall back to revealing the hidden child pane when no visible
pane currently hosts the transcript.

Co-Authored-By: Oz <[email protected]>
Same fix as the orchestration pill bar's 'Focus pane' handler, applied
to the parent-crumb click in render_orchestration_breadcrumbs. The\nbreadcrumb is rendered in a split-off child pane and its parent-crumb\nclick should focus the orchestrator's pane:\n  * Same pane group (sibling pane in this tab) -> dispatch\n    TerminalAction::RevealChildAgent so the pane group can focus the\n    sibling pane from its own ViewContext<PaneGroup>.\n  * Different pane group (other tab / window) -> dispatch\n    WorkspaceAction::FocusTerminalViewInWorkspace.\n  * No canonical owner -> fall back to SwitchAgentViewToConversation\n    so the breadcrumb stays useful when the orchestrator pane has been\n    closed.\n\nPreviously the breadcrumb used WorkspaceAction::RestoreOrNavigateToConversation\nwith nav_data from AgentConversationsModel, which suffered from the\nsame ownership-stomping problem and same-tab focus issues we just\nresolved for the pill bar.\n\nCo-Authored-By: Oz <[email protected]>
Their behaviour isn't reliable enough to ship right now. The action\nvariants and matching TerminalAction handlers are kept in place\n(annotated #[allow(dead_code)]) so re-adding the menu items only\nrequires uncommenting the entries in open_menu_for.\n\nCo-Authored-By: Oz <[email protected]>
@advait-m advait-m marked this pull request as ready for review May 5, 2026 06:12
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 5, 2026

@advait-m

I ran into an unexpected error while working on this.

Powered by Oz

…n-pills-v2

# Conflicts:
#	app/src/ai/agent_conversations_model.rs
#	app/src/ai/agent_sdk/driver.rs
#	app/src/ai/blocklist/history_model.rs
#	app/src/ai/blocklist/orchestration_event_streamer.rs
#	app/src/pane_group/pane/terminal_pane.rs
@advait-m advait-m requested a review from cephalonaut May 5, 2026 06:36
The pill bar / breadcrumb / 3-dot menu UX is ready for the dev team to\nstart using by default. Layered on top of OrchestrationV2, which is\nalready in DOGFOOD_FLAGS.\n\nCo-Authored-By: Oz <[email protected]>
Comment thread app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs
Comment thread app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs Outdated
Copy link
Copy Markdown
Contributor

@cephalonaut cephalonaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking, looking good!

advait-m and others added 2 commits May 5, 2026 09:49
Mirror the existing alive-set retain pattern for the pill-body
mouse-states map onto the overflow-button map. The two maps are kept
in lockstep everywhere else in this file (cleared together on
EnteredAgentView/ExitedAgentView, removed together on
RemoveConversation/DeletedConversation), but ensure_mouse_states
only pruned the pill-body map. That left the overflow-button map
leaking MouseStateHandle entries for old orchestrators / their
children whenever the user switched between orchestrators within
the same view — the same scenario the existing retain comment
calls out for the pill-body map.

Co-Authored-By: Oz <[email protected]>
Drop the hand-rolled shorten_home_path helper, which only checked
the $HOME env var (Unix-specific — Windows uses %USERPROFILE%) and
duplicated logic that already exists as
warp_util::path::user_friendly_path. Switch to
dirs::home_dir() (cross-platform) + the shared user_friendly_path
helper so the cwd line in the hover card displays as ~/foo on every
OS, matching the same tilde-substitution behaviour used by the tab
title, prompt header chip, and pwd context chip.

Co-Authored-By: Oz <[email protected]>
@advait-m advait-m merged commit dcc4cba into master May 5, 2026
25 checks passed
@advait-m advait-m deleted the advait/orchestration-pills-v2 branch May 5, 2026 17:20
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.

2 participants