Orchestration pill bar updates: same-pane pills, 3-dot menu, hover card, breadcrumbs#9680
Merged
Orchestration pill bar updates: same-pane pills, 3-dot menu, hover card, breadcrumbs#9680
Conversation
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]>
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]>
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]>
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]>
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]>
Contributor
…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
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]>
cephalonaut
reviewed
May 5, 2026
cephalonaut
approved these changes
May 5, 2026
Contributor
cephalonaut
left a comment
There was a problem hiding this comment.
Nothing blocking, looking good!
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 sameFocusOpenedConversationaction the menu uses.Open in new pane,Open in new tab.Focus paneitem that focuses the existing pane.Stop agent/Kill agentitems are intentionally hidden for now (the action wiring stays so they can be re-added once the underlying behaviour is reliable).Event::OpenChildAgentInNewTabplumbed up fromTerminalView→pane_group::Event→Workspace, then enters the agent view in the fresh tab.Artifact::PullRequest.[Parent Avatar] Title › [Child Avatar] Name, wrapped in a horizontalNewScrollablewith overlaid scrollbar so a narrow pane can pan and the row stays vertically centered.BlocklistAIHistoryModel, then dispatchTerminalAction::RevealChildAgentwhen the owner pane is in the same pane group (sibling pane in this tab) orWorkspaceAction::FocusTerminalViewInWorkspacewhen it's in a different pane group (other tab / window). Going through the workspace'sfocus_panefrom a foreignViewContextdoesn't reliably move focus to siblings, hence the split.BlocklistAIHistoryEvent::ConversationOwnershipTransferredevent is emitted byset_active_conversation_idwhenever a conversation moves from one terminal view's live list to another.TerminalView::handle_ai_history_model_eventlistens for it and drops staleAIBlock/AgentViewEntryrich 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.Gated by
FeatureFlag::OrchestrationPillBar.Testing
Tested locally on macOS:
Focus paneitem when the conversation is already open in another visible pane.cargo check -p warp,cargo fmt, andcargo clippy --workspace --all-targets --all-features --tests -- -D warningsall pass.