@@ -12877,14 +12877,16 @@ impl Workspace {
1287712877 });
1287812878 }
1287912879
12880- /// Open a local-to-cloud handoff pane next to the active local pane. Triggered
12881- /// by the `/move-to-cloud` slash command and the "Hand off to cloud" footer
12882- /// chip.
12880+ /// Open a local-to-cloud handoff pane in place over the active local pane.
12881+ /// Triggered by the `/move-to-cloud` slash command and the "Hand off to
12882+ /// cloud" footer chip.
1288312883 ///
1288412884 /// When the active conversation is non-empty and has a server token, mints a
12885- /// server-side fork via `POST /agent/conversations/{conversation_id}/fork`, then
12886- /// splits a fresh cloud-mode pane next to the local pane and pre-populates it
12887- /// with the forked conversation.
12885+ /// server-side fork via `POST /agent/conversations/{conversation_id}/fork`,
12886+ /// then pushes a fresh cloud-mode view onto the active pane's navigation stack
12887+ /// and pre-populates it with the forked conversation. Pressing Escape in the
12888+ /// cloud-mode view pops back to the original local pane (the same pattern used
12889+ /// by Cmd-Alt-Enter / Ctrl-Alt-Enter to enter cloud mode from a local session).
1288812890 ///
1288912891 /// All failure modes — ineligibility, fork RPC failure, and local fork
1289012892 /// materialization failure — surface an error toast and **do not open** any
@@ -12914,11 +12916,11 @@ impl Workspace {
1291412916 conversation
1291512917 .server_conversation_token()
1291612918 .cloned()
12917- .map(|token| (conversation.clone(), token))
12919+ .map(|token| (view.clone(), conversation.clone(), token))
1291812920 })
1291912921 });
1292012922
12921- let Some((source_conversation, source_token)) = source else {
12923+ let Some((source_view, source_conversation, source_token)) = source else {
1292212924 // Ineligible: don't open a fresh unrelated pane — the chip is a
1292312925 // hand-off-this-conversation action.
1292412926 let window_id = ctx.window_id();
@@ -12938,6 +12940,7 @@ impl Workspace {
1293812940 move |me, result, ctx| match result {
1293912941 Ok(response) => {
1294012942 me.complete_local_to_cloud_handoff_open(
12943+ source_view,
1294112944 source_conversation,
1294212945 response.forked_conversation_id,
1294312946 initial_prompt,
@@ -12960,11 +12963,23 @@ impl Workspace {
1296012963
1296112964 /// Finishes the local-to-cloud handoff open after the fork RPC returns.
1296212965 /// Materializes a local fork bound to the server's forked conversation id,
12963- /// splits a fresh cloud-mode pane, restores the forked conversation into it,
12964- /// seeds `PendingHandoff`, and kicks off async derivation + snapshot upload.
12966+ /// pushes a fresh cloud-mode view onto `source_view`'s navigation stack,
12967+ /// restores the forked conversation into it, exits the source pane's agent
12968+ /// view (so Esc returns to a clean terminal), seeds `PendingHandoff`, and
12969+ /// kicks off async derivation + snapshot upload.
12970+ ///
12971+ /// The source's agent-view exit happens AFTER the new pane is pushed, so the
12972+ /// source pane is hidden behind the cloud-mode view when its chrome flips
12973+ /// from agent mode to terminal mode — the user never sees the transition.
12974+ /// The pane-stack pop logic in `terminal/view.rs` skips popping when the
12975+ /// `ExitedAgentView` event has `is_exit_before_new_entrance: true`, which
12976+ /// prevents the bookkeeping exit triggered by `restore_conversation_after_view_creation`
12977+ /// (re-entering agent view for the forked conversation) from tearing down
12978+ /// the just-pushed pane.
1296512979 #[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
1296612980 fn complete_local_to_cloud_handoff_open(
1296712981 &mut self,
12982+ source_view: ViewHandle<TerminalView>,
1296812983 source_conversation: AIConversation,
1296912984 forked_conversation_id: String,
1297012985 initial_prompt: Option<String>,
@@ -12997,25 +13012,18 @@ impl Workspace {
1299713012 };
1299813013 let local_fork_id = local_fork.id();
1299913014
13000- self.active_tab_pane_group().update(ctx, |pane_group, ctx| {
13001- pane_group.add_ambient_agent_pane(ctx);
13002- });
13003- let Some(new_pane_view) = self
13004- .active_tab_pane_group()
13005- .as_ref(ctx)
13006- .active_session_view(ctx)
13007- else {
13008- log::warn!(
13009- "start_local_to_cloud_handoff: no active session view after add_ambient_agent_pane"
13010- );
13011- return;
13012- };
13013- let Some(model_handle) = new_pane_view
13014- .as_ref(ctx)
13015- .ambient_agent_view_model()
13016- .cloned()
13017- else {
13018- log::warn!("start_local_to_cloud_handoff: new ambient agent pane has no view model");
13015+ // Push the cloud-mode pane onto the source's nav stack.
13016+ let Some((new_pane_view, model_handle)) = source_view.update(ctx, |view, view_ctx| {
13017+ view.start_local_to_cloud_handoff_pane(view_ctx)
13018+ }) else {
13019+ log::warn!("start_local_to_cloud_handoff: failed to push cloud-mode pane");
13020+ let window_id = ctx.window_id();
13021+ WorkspaceToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
13022+ let toast = DismissibleToast::error(
13023+ "Failed to prepare handoff. Please try again.".to_owned(),
13024+ );
13025+ toast_stack.add_ephemeral_toast(toast, window_id, ctx);
13026+ });
1301913027 return;
1302013028 };
1302113029
@@ -13028,16 +13036,33 @@ impl Workspace {
1302813036 }
1302913037
1303013038 // Restore the forked conversation into the new pane so its AI exchanges are
13031- // visible immediately. Mirrors the `/fork` in-current-pane flow.
13032- let local_fork_for_restore = local_fork.clone();
13039+ // visible immediately. This re-enters agent view for the forked conversation,
13040+ // which emits an internal `ExitedAgentView { is_exit_before_new_entrance: true }`
13041+ // for the cloud-mode placeholder; the pane-stack pop is skipped for that flag.
1303313042 new_pane_view.update(ctx, |terminal_view, view_ctx| {
1303413043 terminal_view.restore_conversation_after_view_creation(
13035- RestoredAIConversation::new(local_fork_for_restore ),
13044+ RestoredAIConversation::new(local_fork.clone() ),
1303613045 /* use_live_appearance */ true,
1303713046 view_ctx,
1303813047 );
1303913048 });
1304013049
13050+ // Enter fullscreen agent view for the restored fork in the new pane. Without this the
13051+ // pane sits in shared-session-viewer-pending mode and the input renders read-only;
13052+ // entering agent view activates the editable cloud-mode input flow. Use
13053+ // `RestoreExistingConversation` to indicate the conversation is restored (not new) —
13054+ // the `is_local_to_cloud_handoff` flag set via `set_pending_handoff` below is the
13055+ // authoritative "this is a cloud agent pane" signal for downstream gating
13056+ // (`is_cloud_agent_pre_first_exchange`, etc.).
13057+ new_pane_view.update(ctx, |terminal_view, view_ctx| {
13058+ terminal_view.enter_agent_view_for_conversation(
13059+ None,
13060+ AgentViewEntryOrigin::RestoreExistingConversation,
13061+ local_fork_id,
13062+ view_ctx,
13063+ );
13064+ });
13065+
1304113066 // Bind the local fork to the cloud-side conversation id and mark it as a shared conversation.
1304213067 history_model.update(ctx, |history_model, _| {
1304313068 history_model.set_server_conversation_token_for_conversation(
@@ -13047,6 +13072,22 @@ impl Workspace {
1304713072 history_model.set_viewing_shared_session_for_conversation(local_fork_id, true);
1304813073 });
1304913074
13075+ // Exit the source pane's fullscreen agent view (if active) so pressing Esc in the
13076+ // cloud-mode pane returns to a terminal-mode source. This runs AFTER the push, so
13077+ // the source pane is hidden behind the cloud-mode view and the transition is
13078+ // invisible to the user. `was_ambient_agent` is false for a local source, so this
13079+ // exit doesn't pop the pane stack.
13080+ let source_agent_view_controller = source_view.as_ref(ctx).agent_view_controller().clone();
13081+ if source_agent_view_controller
13082+ .as_ref(ctx)
13083+ .agent_view_state()
13084+ .is_fullscreen()
13085+ {
13086+ source_agent_view_controller.update(ctx, |controller, ctx| {
13087+ controller.exit_agent_view_without_confirmation(ctx);
13088+ });
13089+ }
13090+
1305013091 let pending = PendingHandoff {
1305113092 forked_conversation_id: forked_conversation_id.clone(),
1305213093 touched_workspace: None,
0 commit comments