Skip to content

Commit c46846f

Browse files
Allow cloud agent view exiting during LRCs (#10966)
## Description There was a bug where you sometimes couldn't exit an ambient agent view when there was an LRC. The issue was that we weren't always checking whether or not the pane was an ambient agent pane before blocking exit on an LRC being in progress. This PR fixes that issue, centralizing all surfaces to call one helper fn that does the correct check. ## Testing - [x] I have manually tested my changes locally with `./script/run` ## Demo https://www.loom.com/share/0e81e231888a42bf958effd28e911183 ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode _This PR was created by [Oz](https://warp.dev/oz) (running Codex)._ --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent fa73295 commit c46846f

4 files changed

Lines changed: 76 additions & 33 deletions

File tree

app/src/ai/blocklist/agent_view/controller.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,10 @@ impl AgentViewController {
431431
.active_block()
432432
.is_active_and_long_running();
433433

434-
// In a non-ambient agent case, users cannot exit the fullscreen agent view with an active long running command.
435-
if is_fullscreen_with_long_running {
434+
// Cloud agent panes do not have the same underlying terminal ownership
435+
// constraint (no local shell process), so long-running third party agent
436+
// commands should not trap the user in agent view.
437+
if is_fullscreen_with_long_running && !model.is_dummy_cloud_mode_session() {
436438
return Err(ExitAgentViewError::LongRunningCommand);
437439
}
438440

app/src/terminal/model/terminal_model.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,11 @@ impl TerminalModel {
14381438
self.is_dummy_cloud_mode_session
14391439
}
14401440

1441+
#[cfg(test)]
1442+
pub fn set_is_dummy_cloud_mode_session(&mut self, value: bool) {
1443+
self.is_dummy_cloud_mode_session = value;
1444+
}
1445+
14411446
pub fn is_shared_ambient_agent_session(&self) -> bool {
14421447
matches!(
14431448
self.shared_session_source_type,

app/src/terminal/view.rs

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ use crate::ai::blocklist::agent_view::{
5454
agent_view_bg_fill, get_agent_view_entry_block_position_id, AgentViewController,
5555
AgentViewControllerEvent, AgentViewDisplayMode, AgentViewEntryBlockParams,
5656
AgentViewEntryOrigin, AgentViewHeaderDisabledTheme, AgentViewHeaderTheme,
57-
AgentViewZeroStateBlock, AgentViewZeroStateEvent, EphemeralMessageModel, ExitAgentViewError,
57+
AgentViewZeroStateBlock, AgentViewZeroStateEvent, EphemeralMessageModel,
5858
ExitConfirmationTrigger, InlineAgentViewHeader, OrchestrationPillBar,
5959
ENTER_OR_EXIT_CONFIRMATION_WINDOW,
6060
};
@@ -4717,20 +4717,6 @@ impl TerminalView {
47174717
callback(self, ctx);
47184718
}
47194719

4720-
fn can_exit_agent_view_for_terminal_view(
4721-
&self,
4722-
ctx: &AppContext,
4723-
) -> Result<(), ExitAgentViewError> {
4724-
match self.agent_view_controller.as_ref(ctx).can_exit_agent_view() {
4725-
Err(ExitAgentViewError::LongRunningCommand)
4726-
if self.can_pop_nested_cloud_agent_view(ctx) =>
4727-
{
4728-
Ok(())
4729-
}
4730-
result => result,
4731-
}
4732-
}
4733-
47344720
/// If the active conversation is a child agent, navigate to the parent
47354721
/// and return `true`; otherwise return `false` so the caller can run
47364722
/// the normal exit-agent-view flow. Cross-tab and swap-target cases
@@ -4771,19 +4757,21 @@ impl TerminalView {
47714757
true
47724758
}
47734759

4774-
fn can_pop_nested_cloud_agent_view(&self, ctx: &AppContext) -> bool {
4775-
self.is_ambient_agent_session(ctx) && self.is_nested_cloud_mode(ctx)
4776-
}
4777-
47784760
/// Exits the active agent, either:
47794761
/// * Exiting agent view for the selected conversation
4780-
/// * Popping the current view off the navigation stack (for cloud mode agents)
4762+
/// * Popping the current view off the navigation stack (for nested cloud mode agents)
4763+
/// Root cloud-mode panes (stack depth ≤ 1) are a no-op — there is nowhere to return to.
47814764
fn exit_agent_view(&mut self, ctx: &mut ViewContext<Self>) {
4782-
// For ambient agent sessions (cloud mode), always pop from pane stack.
4783-
// These sessions are pushed onto a nav stack and have no underlying terminal
4784-
// to return to via the normal agent view exit path.
4765+
// For nested ambient agent sessions (cloud mode), pop from pane stack.
4766+
// Root cloud-mode panes have no parent terminal to return to, so escape
4767+
// is a no-op to avoid leaving the app in a borked state.
47854768
if self.is_ambient_agent_session(ctx) {
4786-
if let Some(pane_stack) = self.pane_stack.as_ref().and_then(|h| h.upgrade(ctx)) {
4769+
if let Some(pane_stack) = self
4770+
.pane_stack
4771+
.as_ref()
4772+
.and_then(|h| h.upgrade(ctx))
4773+
.filter(|stack| stack.as_ref(ctx).depth() > 1)
4774+
{
47874775
pane_stack.update(ctx, |stack, ctx| {
47884776
stack.pop(ctx);
47894777
});
@@ -10755,7 +10743,9 @@ impl TerminalView {
1075510743
let disabled_reason = if is_child_agent {
1075610744
None
1075710745
} else {
10758-
self.can_exit_agent_view_for_terminal_view(ctx)
10746+
self.agent_view_controller
10747+
.as_ref(ctx)
10748+
.can_exit_agent_view()
1075910749
.err()
1076010750
.map(|e| e.to_string())
1076110751
};
@@ -20561,7 +20551,12 @@ impl TerminalView {
2056120551
}
2056220552

2056320553
// Disable escape completely for ambient agents without a parent terminal.
20564-
if self.can_exit_agent_view_for_terminal_view(ctx).is_err() {
20554+
if self
20555+
.agent_view_controller
20556+
.as_ref(ctx)
20557+
.can_exit_agent_view()
20558+
.is_err()
20559+
{
2056520560
return;
2056620561
}
2056720562

@@ -20571,7 +20566,7 @@ impl TerminalView {
2057120566
.block_list()
2057220567
.active_block()
2057320568
.is_active_and_long_running();
20574-
if is_long_running && self.can_pop_nested_cloud_agent_view(ctx) {
20569+
if is_long_running && self.is_ambient_agent_session(ctx) {
2057520570
self.exit_agent_view(ctx);
2057620571
} else if !is_long_running {
2057720572
// During first-time setup, always exit directly without confirmation
@@ -26171,7 +26166,12 @@ impl TypedActionView for TerminalView {
2617126166
// to the in-place exit flow.
2617226167
if self.try_navigate_to_parent_conversation(ctx) {
2617326168
ctx.notify();
26174-
} else if self.can_exit_agent_view_for_terminal_view(ctx).is_ok() {
26169+
} else if self
26170+
.agent_view_controller
26171+
.as_ref(ctx)
26172+
.can_exit_agent_view()
26173+
.is_ok()
26174+
{
2617526175
self.exit_agent_view(ctx);
2617626176
ctx.notify();
2617726177
}

app/src/terminal/view_tests.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use warpui::{
2626
use warpui::{App, ReadModel};
2727

2828
use crate::ai::blocklist::agent_view::toolbar_item::AgentToolbarItemKind;
29+
use crate::ai::blocklist::agent_view::ExitAgentViewError;
2930
use crate::ai::blocklist::block::cli_controller::UserTakeOverReason;
3031
use crate::ai::blocklist::{
3132
agent_view::AgentViewEntryOrigin, BlocklistAIHistoryEvent, BlocklistAIHistoryModel,
@@ -78,6 +79,9 @@ fn add_window_with_cloud_mode_terminal(app: &mut App) -> ViewHandle<TerminalView
7879
let (_, terminal) = app.add_window(WindowStyle::NotStealFocus, |ctx| {
7980
TerminalView::new_for_test_with_cloud_mode(tips_model, None, true, ctx)
8081
});
82+
terminal.update(app, |view, _| {
83+
view.model.lock().set_is_dummy_cloud_mode_session(true);
84+
});
8185
terminal
8286
}
8387

@@ -683,8 +687,14 @@ fn escape_pops_nested_cloud_agent_view_with_long_running_command() {
683687
.lock()
684688
.simulate_long_running_block("sleep 10", "running");
685689

686-
assert!(view.can_pop_nested_cloud_agent_view(ctx));
687-
assert_eq!(view.can_exit_agent_view_for_terminal_view(ctx), Ok(()));
690+
assert!(view.is_ambient_agent_session(ctx));
691+
assert!(view.is_nested_cloud_mode(ctx));
692+
assert_eq!(
693+
view.agent_view_controller()
694+
.as_ref(ctx)
695+
.can_exit_agent_view(),
696+
Ok(())
697+
);
688698
});
689699

690700
assert_eq!(
@@ -703,6 +713,30 @@ fn escape_pops_nested_cloud_agent_view_with_long_running_command() {
703713
})
704714
}
705715

716+
#[test]
717+
fn escape_does_not_exit_root_cloud_agent_view_with_long_running_command() {
718+
App::test((), |mut app| async move {
719+
initialize_app_for_terminal_view(&mut app);
720+
let _agent_view = FeatureFlag::AgentView.override_enabled(true);
721+
let _cloud_mode = FeatureFlag::CloudMode.override_enabled(true);
722+
723+
let terminal = add_window_with_cloud_mode_terminal(&mut app);
724+
725+
terminal.update(&mut app, |view, ctx| {
726+
view.enter_agent_view_for_new_conversation(None, AgentViewEntryOrigin::CloudAgent, ctx);
727+
view.model
728+
.lock()
729+
.simulate_long_running_block("claude", "running");
730+
731+
view.handle_input_event(&InputEvent::Escape, ctx);
732+
733+
// Root cloud-mode pane has no parent terminal to return to,
734+
// so Escape is a no-op and agent view stays active.
735+
assert!(view.agent_view_controller().as_ref(ctx).is_active());
736+
});
737+
})
738+
}
739+
706740
#[test]
707741
fn escape_does_not_exit_local_agent_view_with_long_running_command() {
708742
App::test((), |mut app| async move {
@@ -724,7 +758,9 @@ fn escape_does_not_exit_local_agent_view_with_long_running_command() {
724758
.simulate_long_running_block("sleep 10", "running");
725759

726760
assert!(matches!(
727-
view.can_exit_agent_view_for_terminal_view(ctx),
761+
view.agent_view_controller()
762+
.as_ref(ctx)
763+
.can_exit_agent_view(),
728764
Err(ExitAgentViewError::LongRunningCommand)
729765
));
730766

0 commit comments

Comments
 (0)