16 KiB
Cloud-to-cloud handoff PR 3 tech spec
Context
PR 3 adds the first user-visible cloud-to-cloud follow-up entrypoint. PR 1 added the disabled HandoffCloudCloud flag, typed follow-up API client, and execution-aware task accessors. PR 2 added model-level follow-up orchestration: AmbientAgentViewModel::submit_cloud_followup submits a prompt, waits for a fresh session, and emits FollowupSessionReady, while create_cloud_mode_view already routes that event to viewer::TerminalManager::attach_followup_session in app/src/terminal/view/ambient_agent/mod.rs (69-82). This PR wires that orchestration into the terminal UI, shared-session end paths, task liveness model, replay filtering, and targeted tests.
The tombstone UI remains the generic ConversationEndedTombstoneView, but it now optionally receives an ambient task_id, builds a gated desktop Continue cloud action, and keeps the existing local/desktop actions in app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs (176-262, 486-678). The view enriches display data from AmbientAgentTask and hides local continuation for non-Oz harnesses so third-party harness runs do not offer an unsupported local fork.
Ambient session end handling is split by ownership. Generic TerminalView::on_session_share_ended still performs broad session cleanup, but under CloudModeSetupV2 it can insert one tombstone for non-owned ambient viewer sessions before ending the share. Dedicated on_ambient_agent_execution_ended and on_ambient_agent_session_ended route through handle_non_running_ambient_agent_task, mark the task execution ended, refresh the details panel, and either enable owned follow-up input directly or insert a single tracked tombstone for viewer-style sessions in app/src/terminal/view/shared_session/view_impl.rs (681-879). viewer::TerminalManager::ambient_session_ended remains the narrow path for execution boundaries and does not mark the pane as a finished viewer.
Terminal input submission still assumes an active shared-session network. Input::submit_viewer_ai_query freezes the input, collects context attachments, and emits InputEvent::SendAgentPrompt in app/src/terminal/input.rs (12400-12545). TerminalView::handle_input_event forwards that as TerminalViewEvent::SendAgentPrompt in app/src/terminal/view.rs (19655-19666), and the viewer manager sends it to the current Network in app/src/terminal/shared_session/viewer/terminal_manager.rs (1389-1402). Between cloud executions there is intentionally no current network, so PR 3 needs a separate follow-up submission route.
Cloud Mode setup-v2 already has UI to show startup progress and errors from the ambient model. This PR extends AmbientAgentViewModel with pending_followup_prompt, should_show_followup_progress, and optimistically_rendered_user_queries so follow-up setup can show progress without duplicating prompts after replay in app/src/terminal/view/ambient_agent/model.rs (137-179, 532-578). CloudModeInitialUserQuery is now paired with CloudModeFollowupUserQuery, both backed by the shared render_user_query styling in app/src/terminal/view/ambient_agent/block/query.rs.
Goals
Show a cloud “Continue” entrypoint on eligible ambient Cloud Mode tombstones behind HandoffCloudCloud.
Keep “Continue locally” available and unchanged when the feature flag is off.
Use the existing terminal input/editor for the follow-up prompt instead of embedding a separate tombstone editor.
Route follow-up submission to AmbientAgentViewModel::submit_cloud_followup, not to the ended shared-session Network.
Reuse the setup-v2 loading/error UI while waiting for the new execution session.
Render the submitted follow-up prompt optimistically while setup is in progress.
Preserve the same terminal pane, local conversation, stable run/task ID, and hotswap attach path.
Avoid replaying already-rendered follow-up prompts into the existing conversation transcript.
Non-goals
No first-class attachments support for cloud follow-up prompts unless it falls out naturally from the existing input path. It is acceptable for PR 3 to submit text-only follow-ups and leave file attachment support for a follow-up.
No changes to the server follow-up API.
No rollout enablement for HandoffCloudCloud.
No details-panel redesign or per-execution history UI.
No change to normal non-ambient shared-session ended behavior.
Proposed changes
Ambient execution-ended tombstone insertion
TerminalView::on_ambient_agent_execution_ended(ctx) now delegates to handle_non_running_ambient_agent_task without calling the full generic shared-session cleanup. The method keeps the terminal view/model, input, ambient view model, pane configuration, and shareable object alive for follow-up hotswap. on_ambient_agent_session_ended uses the same helper for task-liveness updates that arrive outside the live shared-session event path.
The helper marks the task execution ended in AgentConversationsModel, refreshes the details panel, and then gates UI updates on HandoffCloudCloud, CloudModeSetupV2, absence of an existing tombstone, and absence of a pending follow-up. Owned ambient panes do not receive a tombstone; if no live shared session remains, they call enable_owned_cloud_followup_input(task_id, ctx) so the user can keep typing in the existing input. Non-owned viewer panes insert the tracked tombstone.
The implementation uses a single tracked tombstone per terminal view via conversation_ended_tombstone_view_id. insert_conversation_ended_tombstone is idempotent and remove_conversation_ended_tombstone removes the card when the user starts a cloud follow-up from it. This avoids repeated cards during task state churn and keeps the UI focused on the current resumable boundary.
Tombstone Continue action
ConversationEndedTombstoneView on desktop now creates an optional Continue cloud button when the tombstone has an ambient task_id and FeatureFlag::HandoffCloudCloud is enabled. Rendering additionally requires AI to be enabled and desktop-only compilation. The terminal-view subscription validates that the current ambient view model still owns the clicked task_id before entering follow-up compose mode.
The tombstone action is ContinueInCloud { task_id }. It records AgentManagementTelemetryEvent::TombstoneContinueInCloud, emits ConversationEndedTombstoneEvent::ContinueInCloud, and lets TerminalView::start_cloud_followup_from_tombstone remove the tombstone, focus the existing input, and set pending_cloud_followup_task_id.
Keep “Continue locally” visible for Oz/plain conversations. When both actions are visible, the cloud Continue button renders first, followed by Continue locally. For non-Oz harnesses, local continuation is hidden because those runs cannot be forked into a local Warp conversation. With HandoffCloudCloud disabled, the cloud button is not created.
Follow-up input mode and submission route
TerminalView owns the follow-up compose state with pending_cloud_followup_task_id: Option<AmbientAgentTaskId>. Tombstone clicks and owned execution end paths call reset_after_cloud_followup_submission, set agent input mode, update pane configuration, and focus the existing input. This keeps the tombstone as a reveal/focus entrypoint rather than an editor.
When InputEvent::SendAgentPrompt arrives, try_submit_pending_cloud_followup intercepts it before the normal TerminalViewEvent::SendAgentPrompt path. It validates the feature flag, ambient model, and task ID, then calls AmbientAgentViewModel::submit_cloud_followup(prompt, ctx). On success it resets the input after submission and returns without emitting to the ended shared-session network. On empty prompts it keeps the compose route active enough to restore agent input. On validation failure it restores the prompt into the input, clears pending follow-up state, focuses input, and shows an error toast.
Slash commands like /fork and /fork-and-compact keep the existing local behavior from Input::submit_viewer_ai_query; the follow-up route handles normal non-empty agent prompts.
Loading UI and optimistic prompt rendering
PR 2 already sets Status::WaitingForSession { kind: Followup } and emits FollowupDispatched. PR 3 reuses that state to render setup-v2 loading UI while polling between executions after a follow-up is submitted.
CloudModeFollowupUserQuery renders the submitted follow-up prompt using the same render_user_query styling as CloudModeInitialUserQuery in app/src/terminal/view/ambient_agent/block/query.rs. It is inserted on FollowupDispatched, not on DispatchedAgent, so initial-run behavior remains unchanged.
The ambient view model exposes pending follow-up state through pending_followup_prompt, should_show_followup_progress, and optimistic query tracking. FollowupDispatched records the prompt once, inserts CloudModeFollowupUserQuery, and the model tracks rendered prompts in optimistically_rendered_user_queries so the same prompt is not rendered again during shared-session replay. Terminal states that end setup clear pending/progress state so rejected submissions do not permanently append optimistic UI.
Error, retry, replay, and state cleanup
API or polling errors reuse the existing AmbientAgentViewModelEvent::Failed, auth, quota, and capacity events so the setup screen can show the same error UI as initial Cloud Mode. If submission fails before the model accepts it, restore_followup_prompt_after_failed_submission restores the prompt into the input, re-enters agent input mode, and focuses the input for retry.
If a follow-up is accepted but fails before a session becomes ready, the local conversation stays with the same task/run ID and the ambient status moves to the existing error/auth/cancelled UI states. Retry continues through the same pending follow-up/input path rather than allocating a new local conversation.
When FollowupSessionReady fires, follow-up compose/input state and pending optimistic state are cleared, and the existing FollowupSessionReady -> attach_followup_session hotswap path attaches the new shared session.
The shared-session replay controller adds should_skip_current_replayed_response and should_skip_replayed_response_for_existing_conversation to avoid duplicating a response that is already represented in the local conversation when replaying the new execution.
Telemetry
Add tombstone cloud-follow-up click telemetry near the existing tombstone telemetry in AgentManagementTelemetryEvent. The implemented event is TombstoneContinueInCloud { task_id }, serialized with the stable task ID. Submission/session-ready/failure outcomes continue to rely on the existing ambient task and follow-up lifecycle telemetry rather than adding separate PR 3-specific events.
End-to-end flow
- A Cloud Mode execution ends and the viewer manager receives
SessionEnded. ambient_session_endedrecords the ended session ID and calls the ambient execution-ended terminal-view method.- The terminal view marks task execution ended, refreshes task/details state, and either enables owned follow-up input directly or inserts one tracked tombstone while keeping the pane/input resumable.
- For non-owned viewer sessions, the user clicks “Continue” on the tombstone.
- The terminal removes the tombstone, focuses the existing input, and marks the next normal agent prompt as a cloud follow-up.
- The user submits a prompt.
- The input/view routes the prompt to
AmbientAgentViewModel::submit_cloud_followup. - The view inserts one optimistic follow-up user query, records it as rendered, and shows setup-v2 loading UI.
- The model submits the follow-up API request and polls for a fresh session.
FollowupSessionReadyreachescreate_cloud_mode_view, which callsattach_followup_session.- The viewer manager joins the new shared session in append mode, and new output streams into the same terminal pane.
Testing and validation
Unit tests cover the shipped seams: task active/joinable helpers and conversation display status in app/src/ai/agent_conversations_model_tests.rs; cloud follow-up compose restoration in app/src/terminal/view_test.rs; tombstone insertion/removal, owned follow-up input, stale task rejection, and task end handling in app/src/terminal/view/shared_session/view_impl_test.rs; and ambient session-end/network behavior plus replay handling in app/src/terminal/shared_session/viewer/event_loop_test.rs.
Remaining useful coverage is tombstone rendering for button visibility with HandoffCloudCloud on/off, with and without task_id, AI enabled/disabled, and local continuation hidden for non-Oz harnesses.
Ambient view coverage verifies that FollowupDispatched inserts/renders an optimistic follow-up prompt separately from DispatchedAgent, records rendered prompts, and clears/re-enables follow-up state for retry.
Viewer manager/event-loop coverage verifies that ambient SessionEnded inserts or enables follow-up UI without setting SharedSessionStatus::FinishedViewer, without sending prompts through a stale network, and without duplicating replayed responses.
Manual validation:
- with
HandoffCloudClouddisabled, complete a Cloud Mode run and verify the tombstone is unchanged; - with the flag enabled, complete a Cloud Mode run, click Continue, submit a prompt, verify setup UI appears, and verify a fresh shared session attaches in the same pane;
- repeat once to catch stale session IDs, duplicate tombstones, and subscription leaks;
- verify “Continue locally” still forks locally from the tombstone;
- verify a normal shared-session viewer still becomes read-only/finished when its session ends.
Targeted validation for this PR is
cargo check -p warp --features handoff_cloud_cloud, focused ambient model/spawn tests from PR 2, and the new tombstone/input/viewer-manager tests. Before opening or updating the PR, follow repo rules for formatting and clippy; do not usecargo fmt --allor file-specificcargo fmt.
Risks and mitigations
Prompt routed to stale network
The largest correctness risk is accidentally sending the follow-up prompt through TerminalViewEvent::SendAgentPrompt to a missing or ended Network. Mitigate by making follow-up compose mode intercept submission before the viewer-manager network path.
Tombstone insertion regresses generic viewer teardown
Generic on_session_share_ended has important cleanup for ordinary shared sessions, but it is too broad for resumable ambient executions. Mitigate by routing execution boundaries through dedicated ambient methods and keeping ordinary shared-session teardown behavior in on_session_share_ended.
Optimistic prompt duplication
The follow-up prompt or response could appear once as optimistic/local UI and again from replayed shared-session scrollback. Mitigate by tracking optimistically_rendered_user_queries in the ambient model and using shared-session replay skip state for already-present conversation responses.
Retry state drift
Failures between API acceptance and session readiness can leave input frozen or the tombstone hidden. Mitigate by centralizing cleanup on ambient model failure/cancel/auth events and ensuring the tombstone remains available.
Stale tombstone state
A tombstone can become stale if task state changes while the view is idle. Mitigate by tracking only one tombstone ID, validating the clicked task_id against the current ambient model before composing, and removing the tombstone when cloud follow-up starts.
Definition of done
With HandoffCloudCloud off, tombstone and shared-session behavior are unchanged.
With the flag on, eligible ambient Cloud Mode tombstones show a cloud Continue action while preserving Continue locally for Oz/plain conversations.
Clicking Continue focuses/reveals the existing terminal input and submitting a normal prompt calls AmbientAgentViewModel::submit_cloud_followup.
The follow-up prompt does not go through the ended shared-session network.
Setup-v2 loading/error UI appears while the follow-up session is starting, and already-rendered replay content is not duplicated.
An optimistic follow-up user query renders during setup without reusing initial-run dispatch UI.
When the new session is ready, the existing FollowupSessionReady hotswap path attaches it to the same pane.
Targeted tests and cargo check -p warp --features handoff_cloud_cloud pass.