Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fca875f
Phase A: same-pane pills + split-off-only breadcrumbs
advait-m Apr 30, 2026
7677c25
Phase B: pin glyph for child agents living in another pane/tab
advait-m Apr 30, 2026
d89e11e
Phase B fixup: pass Fill (not ColorU) to to_warpui_icon
advait-m Apr 30, 2026
8df9dd5
Phase C: action surface for orchestration pill 3-dot menu (UI deferred)
advait-m Apr 30, 2026
08034eb
cargo fmt
advait-m Apr 30, 2026
945467f
Fix pill bar avatars + same-pane click switching regression
advait-m Apr 30, 2026
e60b648
Add 3-dot overflow menu UI to child pills
advait-m Apr 30, 2026
a53787a
Anchor 3-dot menu to the clicked pill's button position
advait-m Apr 30, 2026
d7f4909
Add hover details card overlay for child pills
advait-m Apr 30, 2026
0457a29
Hover card: add status badge, CWD line, harness chip
advait-m Apr 30, 2026
65219b6
Hover card: hide misleading orchestrator status + fix overflow
advait-m Apr 30, 2026
8ed4e05
Clip pill bar to pane bounds so it can't bleed into adjacent panes
advait-m Apr 30, 2026
1c235e4
Make pill row Max-sized so the Clipped wrapper actually clips overflow
advait-m Apr 30, 2026
d76f797
Use vertical 3-dot glyph for the pill overflow button
advait-m Apr 30, 2026
2d0394d
Pill: only show 3-dot glyph when pill is hovered (slot stays reserved)
advait-m Apr 30, 2026
75e43cc
Pill: overlay 3-dot button on hover instead of reserving slot
advait-m Apr 30, 2026
bb4140b
Pill: clip label earlier when dots are visible + stop click bubbling
advait-m Apr 30, 2026
f54022d
Pill: stable label width independent of hover + only highlight select…
advait-m Apr 30, 2026
1e98952
Open in new tab: actually open the child agent in a fresh tab
advait-m Apr 30, 2026
50b3d73
Breadcrumbs: wrap row in horizontal scrollable so narrow panes can pan
advait-m Apr 30, 2026
a305108
Breadcrumbs: overlay scrollbar so the row stays vertically centered
advait-m Apr 30, 2026
43094a5
Breadcrumb parent click: navigate to existing pane/tab when open
advait-m Apr 30, 2026
bfaa2a2
Hover card: add +N -M git diff stats chip per pill
advait-m Apr 30, 2026
b7849be
Pill bar diff stats: diff vs main branch instead of HEAD
advait-m Apr 30, 2026
bb30af7
Hover card: remove diff stats chip for now
advait-m May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/ai/blocklist/agent_view/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,11 @@ impl AgentViewController {
self.pane_group_id
}

/// Returns the [`EntityId`] of the [`TerminalView`] that owns this controller.
pub fn terminal_view_id(&self) -> EntityId {
self.terminal_view_id
}

pub fn set_pane_group_id(&mut self, pane_group_id: EntityId) {
self.pane_group_id = Some(pane_group_id);
}
Expand Down
1,169 changes: 1,127 additions & 42 deletions app/src/ai/blocklist/agent_view/orchestration_pill_bar.rs

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions app/src/pane_group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,12 @@ pub enum Event {
/// Clears the hovered tab index so it no longer appears as highlighted drop target
ClearHoveredTabIndex,
OpenWarpDriveObjectInPane(ObjectUid),
/// Tell the workspace to open the given child agent conversation in a
/// fresh tab. Bubbled up by `TerminalView::Event::OpenChildAgentInNewTab`
/// from the orchestration pill bar's 3-dot menu.
OpenChildAgentInNewTab {
conversation_id: AIConversationId,
},
OpenSuggestedAgentModeWorkflowModal {
workflow_and_id: SuggestedAgentModeWorkflowAndId,
},
Expand Down Expand Up @@ -6279,6 +6285,42 @@ impl PaneGroup {
.map(|session| session.terminal_view(ctx))
}

/// Walk the visible terminal panes in this group looking for one whose
/// terminal view has the given AI conversation as its active agent-view
/// conversation. Used by the orchestration pill bar to focus an
/// already-visible pane (e.g. "Open in new pane" was already used and the
/// user is now clicking the pinned pill in the orchestrator's view).
///
/// Hidden-for-close panes are skipped: a pane that has been closed and is
/// only retained for the undo stack is not a valid focus target.
pub(crate) fn find_visible_terminal_pane_for_conversation(
&self,
conversation_id: AIConversationId,
ctx: &AppContext,
) -> Option<TerminalPaneId> {
for pane_id in self.terminal_pane_ids() {
if FeatureFlag::UndoClosedPanes.is_enabled() && self.is_pane_hidden_for_close(pane_id) {
continue;
}
let Some(terminal_pane_id) = pane_id.as_terminal_pane_id() else {
continue;
};
let Some(terminal_view) = self.terminal_view_from_pane_id(pane_id, ctx) else {
continue;
};
let active_id = terminal_view
.as_ref(ctx)
.agent_view_controller()
.as_ref(ctx)
.agent_view_state()
.active_conversation_id();
if active_id == Some(conversation_id) {
return Some(terminal_pane_id);
}
}
None
}

/// Given a pane ID, retrieve its backing code view, if the pane is a code pane.
pub fn code_view_from_pane_id(
&self,
Expand Down
22 changes: 21 additions & 1 deletion app/src/pane_group/pane/terminal_pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1107,14 +1107,34 @@ fn handle_terminal_view_event(
ctx.emit(pane_group::Event::FreeTierLimitCheckTriggered);
}
Event::RevealChildAgent { conversation_id } => {
// Hidden child pane case: reveal it, then focus.
if let Some(&child_pane_id) = group.child_agent_panes.get(conversation_id) {
group.panes.show_pane_for_child_agent(child_pane_id);
group.handle_pane_count_change(ctx);
group.focus_pane(child_pane_id, true, ctx);
} else if let Some(visible_pane_id) =
group.find_visible_terminal_pane_for_conversation(*conversation_id, ctx)
{
// Already-visible pane case (e.g. a pinned child pill in
// the orchestrator's pill bar): the child has been opened
// in another pane via "Open in new pane"/"Open in new tab"
// and is no longer in `child_agent_panes`. Walk visible
// terminal panes, find the one whose terminal view has
// this conversation active, and focus it in place.
group.focus_pane(visible_pane_id.into(), true, ctx);
} else {
log::warn!("No hidden pane found for child conversation {conversation_id:?}");
log::warn!("No pane found for child conversation {conversation_id:?}");
}
}
Event::OpenChildAgentInNewTab { conversation_id } => {
// The pane group can't add a new tab — only the workspace
// can. Forward the request upward so `WorkspaceView` can
// create a fresh tab and switch its agent view to this
// child conversation.
ctx.emit(pane_group::Event::OpenChildAgentInNewTab {
conversation_id: *conversation_id,
});
}
Event::StartAgentConversation(request) => {
let request = request.clone();
match request.execution_mode.clone() {
Expand Down
82 changes: 80 additions & 2 deletions app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1982,6 +1982,13 @@ pub enum Event {
RevealChildAgent {
conversation_id: AIConversationId,
},
/// Emitted when the user picks "Open in new tab" from a child pill's 3-dot
/// menu in the orchestration pill bar. Bubbles up through `PaneGroup` to
/// `Workspace`, which creates a new tab and switches its agent view to
/// the given child conversation.
OpenChildAgentInNewTab {
conversation_id: AIConversationId,
},
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -2302,6 +2309,13 @@ struct TerminalViewMouseStates {
// Mouse state for the pane header ambient agent indicator tooltip.
ambient_agent_indicator_mouse_handle: MouseStateHandle,
parent_conversation_header_link: MouseStateHandle,
/// Persistent horizontal scroll state for the orchestration breadcrumb
/// row. Lives here (rather than as a `MouseStateHandle`) so the user's
/// scroll position survives across renders — in narrow split-off panes
/// the breadcrumb row often overflows the title slot, and we wrap it
/// in a `NewScrollable::horizontal` keyed on this handle so the user
/// can pan to read clipped labels.
breadcrumbs_horizontal_scroll: ClippedScrollStateHandle,
}

/// Where content was routed when sent to a CLI agent.
Expand Down Expand Up @@ -3999,8 +4013,9 @@ impl TerminalView {
ctx,
)
});
let orchestration_pill_bar =
ctx.add_view(|ctx| OrchestrationPillBar::new(agent_view_controller.clone(), ctx));
let orchestration_pill_bar = ctx.add_typed_action_view(|ctx| {
OrchestrationPillBar::new(agent_view_controller.clone(), ctx)
});
ctx.subscribe_to_view(&orchestration_pill_bar, |_, _, _, ctx| ctx.notify());

let agent_view_back_button = ctx.add_typed_action_view(|ctx| {
Expand Down Expand Up @@ -24485,6 +24500,10 @@ impl TypedActionView for TerminalView {
| ToggleUsageFooter
| RevealChildAgent { .. }
| SwitchAgentViewToConversation { .. }
| OpenChildAgentInNewPane { .. }
| OpenChildAgentInNewTab { .. }
| StopAgentConversation { .. }
| KillAgentConversation { .. }
| OpenCLIAgentRichInput
| ToggleSessionRecording => Empty,
}
Expand Down Expand Up @@ -25522,6 +25541,65 @@ impl TypedActionView for TerminalView {
ctx,
);
}
OpenChildAgentInNewPane { conversation_id } => {
// "Open in new pane": orchestrator-spawned children already
// live in their own hidden pane within the orchestrator's
// pane group, so reveal that pane in place. If the child
// has been promoted out into another visible pane already,
// `RevealChildAgent`'s pane-group handler falls through to
// focusing it via `find_visible_terminal_pane_for_conversation`.
ctx.emit(Event::RevealChildAgent {
conversation_id: *conversation_id,
});
}
OpenChildAgentInNewTab { conversation_id } => {
// "Open in new tab": bubble up to the workspace, which is
// the only layer that can add a new tab. The workspace will
// create a fresh session tab and call
// `enter_agent_view_for_conversation` with this id so the
// new tab opens directly into the child's agent view (not
// the orchestrator's). The current tab stays where it is
// and the workspace switches focus to the new tab as part
// of `add_new_session_tab_with_default_mode`.
ctx.emit(Event::OpenChildAgentInNewTab {
conversation_id: *conversation_id,
});
}
StopAgentConversation { conversation_id } => {
// Cancel the ambient task if this is a cloud agent. For
// local conversations there is no per-conversation cancel
// entry point yet — V2-of-V2 stops at the cloud-side cancel.
if let Some(task_id) = BlocklistAIHistoryModel::as_ref(ctx)
.conversation(conversation_id)
.and_then(|c| c.task_id())
{
crate::ai::ambient_agents::task::cancel_task_with_toast(task_id, ctx);
} else {
// TODO(QUALITY-567): wire local conversation cancel for
// child agents whose run is hosted in this client.
log::info!(
"StopAgentConversation: no task_id for conversation {conversation_id:?}; skipping (local cancel TODO)",
);
}
}
KillAgentConversation { conversation_id } => {
// Best-effort: cancel the ambient run if there is one, then
// remove the conversation from local history. Cloud-side
// deletion is intentionally not done in V2 (see PRODUCT.md
// "Non-goals" — server cleanup is a follow-up).
if let Some(task_id) = BlocklistAIHistoryModel::as_ref(ctx)
.conversation(conversation_id)
.and_then(|c| c.task_id())
{
crate::ai::ambient_agents::task::cancel_task_with_toast(task_id, ctx);
}
conversation_utils::remove_conversation(
*conversation_id,
self.view_id,
false, /* delete_from_cloud */
ctx,
);
}
ToggleSessionRecording => {
self.pty_recorder.update(ctx, |recorder, ctx| {
recorder.toggle_recording(ctx);
Expand Down
34 changes: 34 additions & 0 deletions app/src/terminal/view/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,36 @@ pub enum TerminalAction {
SwitchAgentViewToConversation {
conversation_id: AIConversationId,
},
/// Open a child agent conversation in a separate pane (split off from
/// the orchestrator). Dispatched from the orchestration pill bar's
/// 3-dot overflow menu ("Open in new pane"). For child agents that have
/// a hidden pane in `child_agent_panes` this reveals the existing pane;
/// for already-visible panes it focuses the existing pane.
OpenChildAgentInNewPane {
conversation_id: AIConversationId,
},
/// Open a child agent conversation in a separate tab. V2-of-V2 stub:
/// dispatched from the orchestration pill bar's 3-dot overflow menu
/// ("Open in new tab"). For now this falls back to the same path as
/// `OpenChildAgentInNewPane` until tab-level routing is wired through.
OpenChildAgentInNewTab {
conversation_id: AIConversationId,
},
/// Stop a child agent conversation: cancel the in-flight ambient task
/// (if any) and the local conversation's controller. The conversation
/// itself stays alive so the user can still navigate to it. Dispatched
/// from the orchestration pill bar's 3-dot overflow menu ("Stop agent").
StopAgentConversation {
conversation_id: AIConversationId,
},
/// Kill a child agent conversation: stop it (if running), then remove
/// the conversation from local history. Cloud-side cleanup is intentionally
/// not done in V2 — the user is removing it from their local view.
/// Dispatched from the orchestration pill bar's 3-dot overflow menu
/// ("Kill agent").
KillAgentConversation {
conversation_id: AIConversationId,
},
/// Toggle PTY recording for this session.
ToggleSessionRecording,
/// Open the rich input editor for composing a prompt to send to a CLI agent.
Expand Down Expand Up @@ -711,6 +741,10 @@ impl fmt::Debug for TerminalAction {
ToggleUsageFooter => write!(f, "ToggleUsageFooter"),
RevealChildAgent { .. } => write!(f, "RevealChildAgent"),
SwitchAgentViewToConversation { .. } => write!(f, "SwitchAgentViewToConversation"),
OpenChildAgentInNewPane { .. } => write!(f, "OpenChildAgentInNewPane"),
OpenChildAgentInNewTab { .. } => write!(f, "OpenChildAgentInNewTab"),
StopAgentConversation { .. } => write!(f, "StopAgentConversation"),
KillAgentConversation { .. } => write!(f, "KillAgentConversation"),
ToggleSessionRecording => write!(f, "ToggleSessionRecording"),
OpenCLIAgentRichInput => write!(f, "OpenCLIAgentRichInput"),
}
Expand Down
1 change: 1 addition & 0 deletions app/src/terminal/view/pane_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ impl TerminalView {
if let Some(breadcrumbs) = render_orchestration_breadcrumbs(
self.agent_view_controller.as_ref(app),
self.mouse_states.parent_conversation_header_link.clone(),
self.mouse_states.breadcrumbs_horizontal_scroll.clone(),
app,
) {
return breadcrumbs;
Expand Down
36 changes: 36 additions & 0 deletions app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13511,6 +13511,42 @@ impl Workspace {
}
#[cfg(not(feature = "local_fs"))]
pane_group::Event::RemoteRepoNavigated { .. } => {}
pane_group::Event::OpenChildAgentInNewTab { conversation_id } => {
// "Open in new tab" from the orchestration pill bar's 3-dot
// menu. Spawn a fresh session tab and enter agent view for
// the child conversation. The conversation already lives in
// `BlocklistAIHistoryModel`, so the new terminal view can
// adopt it without any restoration plumbing — just call
// `enter_agent_view_for_conversation` on it.
let conversation_id = *conversation_id;
let window_id = ctx.window_id();
self.add_new_session_tab_with_default_mode(
NewSessionSource::Tab,
Some(window_id),
None, /* chosen_shell */
None, /* conversation_restoration */
true, /* hide_homepage */
ctx,
);
if let Some(terminal_view) = self
.active_tab_pane_group()
.as_ref(ctx)
.active_session_view(ctx)
{
terminal_view.update(ctx, |view, ctx| {
view.enter_agent_view_for_conversation(
None,
AgentViewEntryOrigin::OrchestrationPillBar,
conversation_id,
ctx,
);
});
} else {
log::warn!(
"OpenChildAgentInNewTab: no active terminal view in newly created tab"
);
}
}
pane_group::Event::DroppedOnTabBar { origin, pane_id } => {
if let Some(hovered_tab_index) = self.hovered_tab_index {
match hovered_tab_index {
Expand Down
4 changes: 4 additions & 0 deletions assets/bundled/svg/pin-01.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions crates/warp_core/src/ui/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,14 @@ pub enum Icon {
SwitchHorizontal01,
HeartHand,
MessageChatSquare,
Pin,
}

impl From<Icon> for &'static str {
fn from(icon: Icon) -> &'static str {
match icon {
Icon::Menu => "bundled/svg/layout-left.svg",
Icon::Pin => "bundled/svg/pin-01.svg",
Icon::AtSign => "bundled/svg/at-sign.svg",
Icon::Plus => "bundled/svg/plus.svg",
Icon::Copy => "bundled/svg/copy.svg",
Expand Down
Loading