first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+79
View File
@@ -0,0 +1,79 @@
# Cloud-to-cloud handoff PR 1 tech spec
## Problem statement
The master cloud-to-cloud handoff plan needs an initial mergeable PR that adds the scaffolding required by later follow-up orchestration and UI work without changing end-user behavior. PR 1 should make the client aware of the `HandoffCloudCloud` rollout boundary, add a typed client method for the server follow-up API, and start isolating task/run identity from execution/session-scoped data in the client model.
This PR intentionally does not add the tombstone Continue entrypoint, does not submit follow-ups from the UI, and does not attach a follow-up shared session to an existing terminal. Those behaviors belong in later PRs after the foundational APIs exist.
## Current state
The current branch already adds the UI/model layer needed to attach a new backing shared session to an existing shared-session viewer; that foundation is documented in `specs/REMOTE-1478/TECH.md`. The broader sequencing and intended user flow are documented in `specs/handoff-cloud-cloud/TECH.md`.
Feature flags are defined in `crates/warp_features/src/lib.rs`. `CloudModeSetupV2` already exists near the end of the enum at `crates/warp_features/src/lib.rs:827`, and the rollout arrays are defined at `crates/warp_features/src/lib.rs:852`, `crates/warp_features/src/lib.rs:910`, and `crates/warp_features/src/lib.rs:926`. There is currently no `HandoffCloudCloud` flag. The Cargo feature graph in `app/Cargo.toml` is the right place to encode that `handoff_cloud_cloud` depends on `cloud_mode_setup_v2`.
The public API client already has ambient run methods for spawn, list, and get. The `AIClient` trait includes `spawn_agent`, `list_ambient_agent_tasks`, and `get_ambient_agent_task` at `app/src/server/server_api/ai.rs:799`, and the `ServerApi` implementation posts to `agent/run`, lists `agent/runs`, and gets `agent/runs/{task_id}` at `app/src/server/server_api/ai.rs:1404`. Adjacent public API methods post to run-scoped subresources such as `agent/runs/{task_id}/attachments/prepare` at `app/src/server/server_api/ai.rs:1785`, which is the natural implementation pattern for the follow-up API.
The server follow-up API is `POST /api/v1/agent/runs/{runId}/followups` with a JSON body containing `message`. The server route and request type are in the server worktree at `/Users/zachbai/.warp-dev/worktrees/warp-server/cloud-agent-task-name/public_api/openapi.yaml:644`, `/Users/zachbai/.warp-dev/worktrees/warp-server/cloud-agent-task-name/router/handlers/public_api/agent_webhooks.go:181`, `/Users/zachbai/.warp-dev/worktrees/warp-server/cloud-agent-task-name/router/handlers/public_api/agent_webhooks.go:608`, and `/Users/zachbai/.warp-dev/worktrees/warp-server/cloud-agent-task-name/public_api/types/types.gen.go:1225`.
`AmbientAgentTask` currently exposes execution-scoped fields directly: `session_id`, `session_link`, `conversation_id`, `request_usage`, and `is_sandbox_running` at `app/src/ai/ambient_agents/task.rs:217`. `is_no_longer_running` already combines sandbox liveness with run state at `app/src/ai/ambient_agents/task.rs:257`. `SessionJoinInfo::from_task` reads `task.session_link` and `task.session_id` directly at `app/src/ai/ambient_agents/spawn.rs:32`. Agent management and details code also reads these flattened fields directly for session status, links, conversation dedupe, open actions, and details-panel display, including `app/src/ai/agent_conversations_model.rs:468`, `app/src/ai/agent_conversations_model.rs:515`, `app/src/ai/agent_conversations_model.rs:528`, `app/src/ai/agent_conversations_model.rs:553`, `app/src/ai/agent_conversations_model.rs:1293`, and `app/src/ai/conversation_details_panel.rs:337`.
## Goals
Add a disabled `HandoffCloudCloud` feature flag, with the app Cargo feature depending on `cloud_mode_setup_v2`.
Add typed client support for submitting a follow-up prompt to a run via the public API.
Introduce run/execution-aware accessors on `AmbientAgentTask` that preserve current behavior while giving later PRs a place to route active/latest-execution semantics.
Move the most important existing call sites from direct flattened fields to the new accessors when doing so is behavior-preserving and low-risk.
Add targeted unit tests for the new API serialization/path helper and task accessor behavior.
## Non-goals
No visible cloud conversation tombstone changes.
No terminal input submission changes.
No cloud mode setup UI changes.
No follow-up polling or shared-session hotswap orchestration.
No attempt to parse or store a full run-executions array unless the public API already returns it to the client in the current schema. PR 1 should define seams that can absorb that shape later.
No rollout enablement in `DOGFOOD_FLAGS`, `PREVIEW_FLAGS`, or `RELEASE_FLAGS`.
## Proposed changes
### Feature flag scaffolding
Add `HandoffCloudCloud` to `FeatureFlag` in `crates/warp_features/src/lib.rs`, preserving the enum's chronological ordering. Keep the description concise and product-focused, for example gating cloud-to-cloud continuation of cloud mode conversations.
Do not add the flag to any rollout arrays in PR 1. The flag should be available for local overrides and future PRs, but disabled by default.
Add `handoff_cloud_cloud = ["cloud_mode_setup_v2"]` to `app/Cargo.toml` and map the Cargo feature to `FeatureFlag::HandoffCloudCloud` in `app/src/lib.rs`. This keeps the dependency explicit in the build feature graph without adding rollout dependency tests.
### Follow-up API client
Add a request type near `SpawnAgentRequest` in `app/src/server/server_api/ai.rs`:
`RunFollowupRequest { message: String }`, serialized as `{"message":"..."}`.
Add an `AIClient` method named `submit_run_followup` with parameters `run_id: &AmbientAgentTaskId` and `request: RunFollowupRequest`, returning `anyhow::Result<(), anyhow::Error>`. The current server behavior does not expose a client-needed response payload, so implement with `post_public_api_unit(&build_run_followup_url(run_id), &request)`.
Add a small path helper, for example `build_run_followup_url(run_id: &AmbientAgentTaskId) -> String`, mirroring `build_list_agent_runs_url` at `app/src/server/server_api/ai.rs:521`. This makes unit testing the endpoint path independent from HTTP mocking.
Do not call this new method from UI or orchestration code in PR 1.
### Run/execution-aware task accessors
Add lightweight accessors to `AmbientAgentTask` in `app/src/ai/ambient_agents/task.rs`. For PR 1 these should project the existing flattened fields as the active/latest execution:
`run_id() -> AmbientAgentTaskId` returning `task_id`.
`conversation_id() -> Option<&str>` returning `conversation_id.as_deref()`.
`active_run_execution() -> RunExecution<'_>` returning a borrowed projection of the flattened execution-scoped fields.
Add `RunExecution<'a>` with borrowed `session_id`, non-empty `session_link`, `request_usage`, and `is_sandbox_running` fields. The key is that callers stop encoding the assumption that task-level fields are intrinsically task-scoped.
Update `is_no_longer_running` to use `active_run_execution().is_sandbox_running` and preserve existing behavior.
Move `SessionJoinInfo::from_task` in `app/src/ai/ambient_agents/spawn.rs:32` to use `active_run_execution()`.
Update low-risk UI/model call sites that only need the current projection:
`ConversationOrTask::session_id` should parse `active_run_execution().session_id`.
`link_preference` should use `active_run_execution().is_sandbox_running`.
`session_or_conversation_link` should use `active_run_execution().session_link` and `conversation_id()`.
`get_session_status` should use the session/link accessors.
`get_open_action` and conversation shadowing should use `run_id()` and `conversation_id()`.
`ConversationDetailsData::from_task` should use `conversation_id()` for the details-panel conversation id and `active_run_execution().request_usage` for credits.
Avoid broad mechanical churn in call sites where fields are clearly task metadata rather than execution data, such as title, prompt, state, source, creator, artifact list, and config snapshot.
### Optional naming cleanup
Do not rename `AmbientAgentTaskId` or public UI labels in PR 1. The server and much of the client still use run/task terminology interchangeably, and a broad rename would create churn without improving mergeability. The new `run_id()` accessor is enough to clarify stable identity for later PRs.
## Testing strategy
Add public API helper tests in `app/src/server/server_api/ai_test.rs` for the follow-up endpoint path. If the request type is public enough to serialize directly, add a serialization test asserting the JSON shape is exactly `{"message":"..."}`.
Add or update ambient task tests in `app/src/ai/ambient_agents/spawn_tests.rs` or a new `app/src/ai/ambient_agents/task_tests.rs` to cover:
`SessionJoinInfo::from_task` still prefers server-provided session links.
It still falls back to constructing a join link from `session_id`.
It returns `None` when neither an active link nor parseable session id exists.
The new accessors preserve current flattened-field behavior.
Run targeted checks after implementation:
`cargo nextest run -p warp server::server_api::ai::tests::build_run_followup_url_routes_to_run_followups server::server_api::ai::tests::serialize_run_followup_request ai::ambient_agents::spawn::tests`
`cargo nextest run -p warp ai::agent_conversations_model::tests ai::conversation_details_panel::tests`
`cargo check -p warp --features handoff_cloud_cloud`
The exact package names should be verified during implementation from `Cargo.toml` before running. Do not use `cargo fmt --all` or file-specific `cargo fmt`; if formatting is needed before review, use the repo’s standard `cargo fmt` per project guidance.
## Rollout and compatibility
The flag is disabled by default, so PR 1 should not change runtime behavior.
The new API client method is unused in PR 1 and should therefore be safe to merge before server rollout, as long as it compiles against existing client code.
The accessor migration should be behavior-preserving because each accessor initially projects the same flattened fields. If a direct field use is ambiguous or risky, leave it in place and document it as follow-up rather than expanding the PR scope.
## Risks and mitigations
The largest risk is accidentally changing management view link selection or session-open behavior while replacing direct field reads. Keep changes small, prefer local accessor substitutions, and rely on existing spawn and agent management tests where available.
The follow-up endpoint response shape may differ from the assumed empty response. During implementation, verify the server contract before choosing `post_public_api_unit`; if the server returns a body, add a minimal response type and test deserialization.
Runtime implication of `HandoffCloudCloud` to `CloudModeSetupV2` is intentionally not implemented in PR 1. The Cargo feature dependency covers compiled builds, while local runtime overrides can still force unusual states for targeted testing.
The accessor names may need to change once the client consumes first-class run-execution data. Keep PR 1 names descriptive but avoid adding a large abstraction that is not backed by current API data.
## Definition of done
`HandoffCloudCloud` exists and is disabled by default.
`handoff_cloud_cloud` in `app/Cargo.toml` depends on `cloud_mode_setup_v2`.
`AIClient` and `ServerApi` expose a typed follow-up submission method for `POST agent/runs/{run_id}/followups`.
`AmbientAgentTask` exposes run/execution-aware accessors and the main session/conversation call sites use them without behavior changes.
Targeted tests cover the follow-up API path/serialization and task/session accessor behavior, and a feature-gated compile check passes.
@@ -0,0 +1,69 @@
# Cloud-to-cloud handoff PR 2 tech spec
## Problem statement
PR 2 should add the orchestration layer that turns an existing cloud agent run into a follow-up execution and hands the resulting fresh shared session to the hotswap path. This PR should remain mergeable while `HandoffCloudCloud` is disabled by default, and it should not add the user-visible tombstone Continue button or terminal-input submission route yet.
The intended boundary is model/API behavior: a future UI can call one ambient-model method with a follow-up prompt, the client submits `POST agent/runs/{runId}/followups`, polls the same run until a new joinable session appears, ignores the ended session, and emits `FollowupSessionReady` so the existing viewer manager attaches the new session in append mode.
## Current state
PR 1 added the disabled `HandoffCloudCloud` flag and encoded the Cargo feature dependency on `cloud_mode_setup_v2` in `app/Cargo.toml:924`. It also added `RunFollowupRequest`, `AIClient::submit_run_followup`, and `build_run_followup_url` in `app/src/server/server_api/ai.rs (211-216, 837-840, 1467-1471)`, with endpoint/serialization tests in `app/src/server/server_api/ai_test.rs (988-1000)`.
`AmbientAgentTask` now has `run_id()`, `conversation_id()`, and `active_run_execution()` accessors that project the current flattened response fields into a `RunExecution` view in `app/src/ai/ambient_agents/task.rs (247-279)`. `SessionJoinInfo::from_task` already consumes that projection in `app/src/ai/ambient_agents/spawn.rs (31-57)`.
Initial cloud startup is still a single combined helper: `spawn_task` creates a run, polls `get_ambient_agent_task`, emits state changes, and ends when the first session is joinable in `app/src/ai/ambient_agents/spawn.rs (85-176)`. There is no reusable “poll an existing run until a new execution session is ready” helper yet.
`AmbientAgentViewModel` still models startup as `Status::WaitingForSession { progress }` without distinguishing an initial run from a follow-up execution in `app/src/terminal/view/ambient_agent/model.rs (50-64)`. The existing `SessionStarted` handler infers follow-up readiness from being already in `AgentRunning`, which is too implicit for a model-driven follow-up flow in `app/src/terminal/view/ambient_agent/model.rs (627-638)`. The current `attach_followup_session` method simply emits `FollowupSessionReady` for a known session ID, which is useful test scaffolding but does not submit or poll a follow-up in `app/src/terminal/view/ambient_agent/model.rs:349`.
The hotswap receiver already exists. `create_cloud_mode_view` routes `SessionReady` to `connect_to_session` and `FollowupSessionReady` to `attach_followup_session` in `app/src/terminal/view/ambient_agent/mod.rs (69-82)`. The viewer manager’s follow-up attach path replaces the active network and joins with append-mode scrollback in `app/src/terminal/shared_session/viewer/terminal_manager.rs (338-384)`.
The UI has important side effects tied to initial dispatch. `DispatchedAgent` inserts the initial optimistic user query in `TerminalView::handle_ambient_agent_event` and drives the ambient entry-block insertion subscription in `app/src/terminal/view/ambient_agent/view_impl.rs (105-131, 445-481)`. PR 2 should avoid reusing that event for follow-ups, because doing so would blur initial-run and follow-up behavior before the UX PR.
## Goals
Add reusable follow-up orchestration that submits a prompt to an existing run and waits for a fresh active execution session.
Make the ambient view model explicitly track whether it is waiting for an initial session or a follow-up session.
Track the active or previous execution session ID so follow-up polling can ignore stale readiness from the ended session.
Emit the already-supported `FollowupSessionReady` event when the new session is ready, allowing the existing hotswap path to attach it.
Reuse existing Cloud Mode setup/loading/error state machinery for follow-up waiting and failures, but without adding a visible Continue entrypoint.
Keep the implementation behind `FeatureFlag::HandoffCloudCloud` and preserve behavior with the flag off.
## Non-goals
No tombstone Continue button, action, or copy changes.
No terminal input routing changes for submitting follow-up prompts.
No embedded follow-up prompt editor in the tombstone.
No product decision on tombstone stacking or update-in-place behavior.
No first-class server execution-array parsing unless the public API response shape already exposes it in this branch.
No rollout enablement for `HandoffCloudCloud`.
## Proposed changes
### Reusable run polling and follow-up helper
Refactor `spawn_task` in `app/src/ai/ambient_agents/spawn.rs` so run creation and run readiness monitoring are separate. Keep the public `spawn_task(request, ai_client, timeout)` behavior the same by having it call a new internal polling helper after `spawn_agent` succeeds.
Add a helper such as `poll_run_until_joinable_session(run_id, ai_client, previous_session_id, timeout)` that repeatedly calls `get_ambient_agent_task(&run_id)`, emits `StateChanged` when state changes, and returns `SessionStarted` only when the task is `InProgress` and `SessionJoinInfo::from_task` contains a parseable `session_id` that differs from `previous_session_id` when one was provided.
Add a follow-up stream/helper such as `submit_run_followup(prompt, run_id, previous_session_id, ai_client, timeout)`. It should call `AIClient::submit_run_followup(run_id, RunFollowupRequest { message: prompt })` first, then call the polling helper. API failure before acceptance should yield an error without polling. Polling errors should surface through the same error path as initial spawn.
For initial spawn, preserve the existing tolerance for a session link without a parsed session ID if any caller still needs that metadata. For follow-up readiness, require a parsed session ID because the hotswap API needs a `SessionId`.
Terminal states before a fresh session is found should not leave the follow-up wait indefinitely. Failure-like states should emit the state change and then surface the task status message as an error; successful terminal completion without a new session should complete with a clear “no follow-up session became available” error.
### Explicit ambient model startup kind
Add a small enum such as `SessionStartupKind { InitialRun, Followup }` and change `Status::WaitingForSession` to carry `{ progress, kind }`. Existing accessors like `agent_progress()` and `is_waiting_for_session()` should remain behavior-preserving.
Add fields to `AmbientAgentViewModel` for follow-up bookkeeping: the active execution `SessionId`, the last ended execution `SessionId` if available, and the currently submitted follow-up prompt. The prompt field is for PR 3’s optimistic rendering; PR 2 should store it but not insert a visible follow-up query block.
Update initial spawn to set `WaitingForSession { kind: InitialRun }`. Update `AmbientAgentEvent::SessionStarted` handling to emit `SessionReady` for `InitialRun` and `FollowupSessionReady` for `Followup`, rather than relying on whether the current status happens to be `AgentRunning`.
Add `AmbientAgentViewModel::submit_cloud_followup(prompt, ctx)`. It should require `FeatureFlag::HandoffCloudCloud`, require an existing `task_id`/run ID, capture the previous active or ended session ID, set `WaitingForSession { kind: Followup }`, start the progress timer, store the pending prompt, emit a distinct follow-up dispatch event, and spawn the follow-up helper stream.
On follow-up success, stop the timer, set `status` to `AgentRunning`, update the active execution session ID, clear the pending prompt, and emit `FollowupSessionReady { session_id }`. On failure, reuse the existing failure/auth/quota/capacity mapping logic as much as possible so follow-up setup errors render through the same state as initial setup errors.
### Execution-ended bookkeeping without visible UI
Extend the ambient session-ended path only enough for bookkeeping. `viewer::TerminalManager::ambient_session_ended` currently leaves the pane resumable and clears the active network in `app/src/terminal/shared_session/viewer/terminal_manager.rs (1490-1515)`. In PR 2 it can notify the ambient view model of the ended session ID behind `HandoffCloudCloud`, so the model records `last_ended_execution_session_id` and can reject duplicate readiness from that session.
This notification should not call `TerminalView::on_session_share_ended`, should not insert a tombstone, should not set `SharedSessionStatus::FinishedViewer`, and should not cancel the local conversation. Those UI and lifecycle decisions remain PR 3 scope.
### Event and view integration
Add a new model event such as `FollowupDispatched` instead of reusing `DispatchedAgent`. `create_cloud_mode_view` only needs an exhaustive-match update for the new event because `FollowupSessionReady` is already wired to `attach_followup_session`.
Update `TerminalView::handle_ambient_agent_event` to handle `FollowupDispatched` by notifying/re-rendering progress UI and marking the active ambient conversation as `ConversationStatus::InProgress` if one exists. It should not insert `CloudModeInitialUserQuery`, should not insert a second `AmbientAgentEntryBlock`, and should not auto-open new UI beyond the existing setup/progress rendering.
The existing loading screen in `app/src/terminal/view/ambient_agent/view_impl.rs (529-571)` can continue to derive messages from `AgentProgress` for PR 2. If copy changes are desired for follow-ups, keep them minimal and keyed off `SessionStartupKind`, but deferring user-facing copy to PR 3 is acceptable.
## Testing strategy
Add stream-level tests in `app/src/ai/ambient_agents/spawn_tests.rs` covering follow-up submission and polling. The important cases are: the helper calls `submit_run_followup` before polling; it ignores the previous session ID returned by the server; it emits `SessionStarted` for a different new session ID; it propagates API errors before polling; it surfaces terminal failure before readiness.
Preserve existing `spawn_task` tests so initial spawn behavior remains unchanged after the refactor.
Add model-level tests if there is a lightweight existing harness for `AmbientAgentViewModel`; otherwise keep model changes small and validate via stream tests plus targeted compile checks. Model assertions should cover `submit_cloud_followup` preconditions, `WaitingForSession { kind: Followup }`, and `FollowupSessionReady` emission on a fresh session.
Run targeted validation after implementation: `cargo nextest run -p warp ai::ambient_agents::spawn::tests server::server_api::ai::tests::build_run_followup_url_routes_to_run_followups server::server_api::ai::tests::serialize_run_followup_request` and `cargo check -p warp --features handoff_cloud_cloud`. If model or terminal-view tests are added, include their module filters. Do not use `cargo fmt --all` or file-specific `cargo fmt`; use the repo’s standard formatting command only when preparing a PR update.
## Rollout and compatibility
With `HandoffCloudCloud` off, no production UI should call the new follow-up method and existing initial Cloud Mode startup should behave as it does today.
With the flag on, PR 2 only exposes an internal/model-level follow-up path. The absence of a visible entrypoint makes this safe to merge before product UX lands, while unit tests can still exercise the orchestration path.
The runtime code may assume `CloudModeSetupV2` when `HandoffCloudCloud` is enabled because the Cargo feature dependency was added in PR 1.
## Risks and mitigations
The server may briefly return the ended execution’s session fields after accepting a follow-up. Mitigate by passing the previous session ID into the polling helper and requiring a different parsed session ID before emitting readiness.
Reusing `DispatchedAgent` for follow-ups would insert initial-run UI artifacts again. Mitigate with a distinct follow-up event and explicit startup kind.
Refactoring `spawn_task` could regress initial Cloud Mode startup. Mitigate by preserving the public stream contract and keeping existing spawn tests green.
A follow-up may be accepted but fail before any session becomes joinable. Mitigate by reusing the existing failed/auth/quota/capacity UI states and leaving future UI free to retry from the tombstone in PR 3.
Model bookkeeping could drift if session-ended notifications are missed. Mitigate by also falling back to the last active execution session ID when submitting a follow-up.
## Parallelization
This PR is small enough to implement sequentially, but two independent tracks could run in parallel if needed. One track can refactor and test `spawn.rs` follow-up polling with mocked `AIClient`; the other can wire `AmbientAgentViewModel` state/events and terminal-manager bookkeeping. They converge at `submit_cloud_followup` consuming the follow-up helper and emitting `FollowupSessionReady`.
## Definition of done
`spawn_task` still behaves the same for initial runs after extracting reusable polling.
A follow-up helper submits a prompt, polls the stable run, ignores stale session IDs, and returns a fresh joinable session.
`AmbientAgentViewModel::submit_cloud_followup` exists behind `HandoffCloudCloud` and drives `WaitingForSession { kind: Followup }` through success and error states.
`FollowupSessionReady` is emitted for fresh sessions and continues to attach through the existing hotswap path.
No tombstone Continue UI or terminal-input follow-up route is added in this PR.
Targeted tests and `cargo check -p warp --features handoff_cloud_cloud` pass.
@@ -0,0 +1,39 @@
# Cloud-to-cloud handoff resumable completed conversations
## Summary
Completed cloud agent conversations should be resumable in cloud mode when cloud-to-cloud handoff is available. Opening a completed cloud task should restore a Cloud Mode pane seeded with the prior conversation history, preserving the familiar completed transcript/tombstone experience while allowing the user to submit a follow-up prompt that starts a new cloud execution in the same conversation.
## Problem
Before cloud-to-cloud handoff, a cloud task with no active execution was permanently complete from the client’s perspective, so the transcript viewer and “Continue locally” affordance were sufficient. With multi-execution cloud runs, the same visual state can now be a pause between executions. Users should not hit a dead-end toast when the product offers a cloud Continue action.
## Figma
Figma: none provided. The current failure state is represented by the screenshot attached to the request.
## Behavior
1. When a user opens a cloud agent conversation that has no active execution and cloud-to-cloud continuation is available, Warp opens a Cloud Mode pane seeded with the completed conversation history instead of starting a new blank task.
2. The restored Cloud Mode pane preserves the existing completed-conversation presentation:
- The prior conversation output remains visible.
- The completed task tombstone remains visible at the end of the transcript.
- Existing metadata, artifacts, error status, runtime, credits, source, skill, and working-directory details remain available wherever they are already shown.
3. For a resumable cloud task, the tombstone shows a primary `Continue` action for continuing in cloud mode.
4. The existing `Continue locally` action remains available where it is supported today. Adding cloud continuation must not remove the local fork path.
5. If the task cannot be continued in cloud mode, Warp does not show a cloud `Continue` action. Users should not be offered an action that can only produce a generic “couldn't continue” failure.
6. Clicking cloud `Continue` does not immediately create a new execution. It prepares the same Cloud Mode pane for a follow-up prompt and focuses the existing terminal input.
7. While the pane is waiting for a follow-up prompt, the user can type, edit, or abandon the prompt using the normal terminal input behavior.
8. Submitting a non-empty follow-up prompt starts a new execution for the same cloud task/run, not a new user-visible conversation.
9. Submitting an empty follow-up prompt does not start a new execution. The pane stays usable and focused so the user can type a real prompt or leave the view.
10. After the follow-up prompt is submitted, the restored pane transitions into Cloud Mode setup/progress UI in the same pane.
11. During follow-up setup, the user sees their submitted prompt represented optimistically so the pane does not appear to ignore the input while the new cloud execution is starting.
12. When the new execution starts, Warp attaches to the new cloud session in the same pane. It does not open a separate tab, split, or replacement conversation unless the user separately chooses another navigation action.
13. The same logical conversation/run identity is preserved across the original transcript and every follow-up execution.
14. New output from the follow-up appears after the existing transcript content in chronological order.
15. Returning to the conversation list, agent management view, details panel, or another navigation surface should focus the already-open pane for this task while it is open.
16. If a follow-up execution finishes, the pane returns to a completed-between-executions state and can offer cloud `Continue` again when the task is still resumable.
17. Repeated follow-ups are allowed. A second or later follow-up should behave the same as the first: prompt input, setup/progress, same-pane session attachment, and preserved conversation identity.
18. A failed, blocked, cancelled, timed-out, unauthorized, or quota-limited follow-up attempt surfaces through the same user-facing Cloud Mode error/auth/capacity/credits states used by cloud task startup.
19. If the follow-up request fails before the prompt is accepted, the user’s prompt is restored in the input so they can edit or retry.
20. If the follow-up request is accepted but the new execution fails before a session is available, the pane shows the appropriate failure state and does not silently discard the completed conversation history.
21. Closing the pane while a follow-up execution is starting stops only the local viewing/waiting experience. It must not imply that the remote cloud run is cancelled unless the user explicitly invokes a cancel action.
22. Opening a completed local agent conversation still uses the existing local transcript behavior. Cloud continuation is only for cloud/ambient agent conversations that are resumable in cloud mode.
23. Opening a non-owned or otherwise non-resumable cloud transcript still works as a read-only transcript. It must not incorrectly expose cloud continuation.
24. If the cloud-to-cloud continuation feature is unavailable or disabled, completed cloud conversations continue to use the existing transcript/tombstone behavior.
25. Existing transcript share/open behavior should continue to work while the view is idle between cloud executions.
26. Keyboard focus should move to the prompt input after cloud `Continue` is clicked. Users should not need to click the input manually before typing the follow-up prompt.
27. The UI should avoid stacking confusing duplicate tombstones for the same between-executions state. After a follow-up starts, the old tombstone should not remain as an actionable end marker above the active setup/progress state.
28. The generic “Couldn't continue this cloud task.” toast is only acceptable for genuinely unexpected inconsistencies. It should not be part of the normal completed-cloud-task continuation flow.
@@ -0,0 +1,91 @@
# Cloud-to-cloud handoff resumable completed cloud panes
## Context
`PRODUCT.md` defines the target behavior: completed cloud conversations should restore into Cloud Mode panes seeded with prior conversation history, keep the completed transcript/tombstone presentation, and become resumable in cloud mode when cloud-to-cloud handoff is available.
The follow-up execution path mostly exists. `ConversationEndedTombstoneView` creates a cloud `Continue` action when it has an ambient task id and `FeatureFlag::HandoffCloudCloud` is enabled in `app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs (202-235)`, then emits `ConversationEndedTombstoneEvent::ContinueInCloud` from `app/src/terminal/view/shared_session/conversation_ended_tombstone_view.rs (631-640)`. `TerminalView::start_cloud_followup_from_tombstone` sets `pending_cloud_followup_task_id` and focuses the existing input in `app/src/terminal/view/shared_session/view_impl.rs (810-842)`. `TerminalView::try_submit_pending_cloud_followup` routes the next prompt through `AmbientAgentViewModel::submit_cloud_followup` in `app/src/terminal/view.rs (19979-20075)`. The ambient model emits `FollowupSessionReady` after polling for a fresh session in `app/src/terminal/view/ambient_agent/model.rs (686-744)`, and the deferred Cloud Mode manager attaches it through `TerminalManager::attach_followup_session` in `app/src/terminal/view/ambient_agent/mod.rs (60-118)` and `app/src/terminal/shared_session/viewer/terminal_manager.rs (310-384)`.
The follow-up submit trigger should be explicit Cloud Mode behavior, not a side effect of shared-session viewer submission. Fresh Cloud Mode prompts already bypass `Input::submit_ai_query` while the ambient model is in `Composing` and call `AmbientAgentViewModel::spawn_agent` directly. Follow-up prompts should follow the same pattern: before generic AI submission and before generic shared-session viewer permission checks, `Input` should detect a disconnected Cloud Mode follow-up composer and emit a dedicated Cloud follow-up event handled by `TerminalView::try_submit_pending_cloud_followup`. Both cases must use this same path: a pane whose initial cloud execution just ended, and a pane restored from ambient conversation history.
Disconnected Cloud Mode panes should also stop representing themselves as shared-session viewers. `SharedSessionStatus::ViewPending`, `ActiveViewer`, and `FinishedViewer` should mean a live or ended collaborative viewer state with shared-session permission semantics. A fresh, restored, or between-executions Cloud Mode pane that has no attached session should instead use `SharedSessionStatus::NotShared` plus the existing Cloud Mode/ambient model state. `connect_session` and `attach_followup_session` should transition into `ViewPending` only when a real shared-session id is available.
The broken path is pane construction. `TerminalView::new` creates `ambient_agent_view_model` only when `is_cloud_mode` is true in `app/src/terminal/view.rs (3003-3030)`. Historical transcript panes are currently created through `MockTerminalManager::create_model` in `app/src/pane_group/mod.rs (5675-5728)` and loaded through `PaneGroup::load_data_into_transcript_viewer` in `app/src/pane_group/mod.rs (3825-3928)`. Those panes can have `ConversationTranscriptViewerStatus::ViewingAmbientConversation(task_id)`, but that is a workaround for treating a generic transcript viewer like an ambient session. It still does not give the pane the ambient model or deferred viewer manager needed to submit and attach a cloud follow-up.
`WorkspaceAction::OpenConversationTranscriptViewer` already carries `ambient_agent_task_id`, but the action handler discards it and calls `load_cloud_conversation_into_new_transcript_viewer(conversation_id, ctx)` in `app/src/workspace/view.rs (21677-21698)`. The loader then creates a generic transcript-loading tab in `app/src/workspace/view.rs (3998-4054)`. By the time cloud conversation data is loaded, `PaneGroup` can recover an ambient task id from metadata, but it still mutates a non-cloud transcript viewer rather than creating a resumable cloud viewer.
The desired invariant is that viewing any ambient agent conversation, including a restored completed one, puts the user in a Cloud Mode pane. The fetched conversation history is seeded UI state for scrollback and completed-tombstone presentation, not the server-side agent context for the next follow-up request. Generic transcript-viewer state should remain only for non-ambient conversations and loading/fallback paths.
## Proposed changes
### Preserve ambient task identity through restore loading
Change `Workspace::load_cloud_conversation_into_new_transcript_viewer` to accept `ambient_agent_task_id: Option<AmbientAgentTaskId>`, and pass the action’s task id through from `WorkspaceAction::OpenConversationTranscriptViewer`.
When `load_conversation_from_server` returns `CloudConversationData`, resolve the effective task id as:
1. the task id from the action, if present;
2. the task id from cloud conversation metadata, if present;
3. `None`.
Use the effective task id to choose the pane construction path, seed the ambient model, register active ambient views, and decide whether the tombstone can offer cloud `Continue`.
### Route ambient restores into Cloud Mode pane construction
Add a PaneGroup helper that creates a restored ambient Cloud Mode pane when all of these are true:
- `FeatureFlag::HandoffCloudCloud` is enabled;
- an effective ambient task id exists;
- the loaded conversation is a cloud/ambient conversation, not a purely local transcript.
A concrete shape:
- `PaneGroup::create_restored_ambient_cloud_mode_pane(conversation, task_id, resources, initial_size, ctx)`
- `PaneGroup::replace_loading_pane_with_restored_ambient_cloud_mode_pane(loading_pane_id, cloud_conversation, task_id, ctx)`
- or a branch in the cloud-conversation loading callback that replaces the loading pane with a Cloud Mode pane instead of calling the generic transcript restoration path.
The helper should use `terminal::view::ambient_agent::create_cloud_mode_view` or the existing `PaneGroup::create_cloud_mode_terminal` wrapper, then restore the fetched conversation history into that view. This gives the view `ambient_agent_view_model`, the `FollowupSessionReady` subscription, and a deferred viewer manager that can later call `attach_followup_session`.
Do not create a `MockTerminalManager` transcript viewer and then retrofit Cloud Mode behavior onto it. Also do not set `ConversationTranscriptViewerStatus::ViewingAmbientConversation(task_id)` for the new path. The pane should be ambient because it is a Cloud Mode pane, not because a transcript-viewer marker carries a task id.
### Seed the Cloud Mode pane with historical UI state
After creating the restored Cloud Mode pane:
- restore the fetched conversation into the terminal view so prior blocks and rich content render as historical UI state;
- insert the completed-conversation tombstone at the end of the restored history;
- call `AmbientAgentViewModel::enter_viewing_existing_session(task_id, ctx)` so the model stores the stable task id and fetches run config metadata;
- if the loaded conversation has a local `AIConversationId`, call `set_conversation_id(Some(id))`;
- register `ActiveAgentViewsModel::register_ambient_session(terminal_view.id(), task_id, ctx)`;
- enter the same agent-view/header/details state as a normal Cloud Mode pane for that task.
This should mirror existing remote-child restoration in `app/src/pane_group/mod.rs (3161-3258)` and the existing ambient Cloud Mode construction path in `app/src/pane_group/mod.rs (3249-3276)`. The restored history is only the pane’s initial UI state. `submit_cloud_followup` should still send the follow-up prompt and task/session identity through the existing cloud-to-cloud follow-up API rather than replaying the transcript as prompt context.
If the current `AmbientAgentViewModel::Status::AgentRunning` is too strong for a completed task with no live session, introduce or reuse a between-executions state that represents “viewing an existing ambient task with no active session.” The important behavior is that the pane looks like the completed Cloud Mode state produced after a fresh Cloud Mode execution ends and the VM/session is no longer active.
When an ambient shared session ends, owner panes should transition back to this disconnected Cloud Mode follow-up composer state and use `SharedSessionStatus::NotShared`. Non-owner panes should remain read-only ended viewer surfaces and must not expose editable Cloud follow-up input.
### Use one Cloud follow-up submission path
Do not maintain separate follow-up submission paths based on whether the pane was previously attached to a shared session. The post-session-ended case and restored-from-history case should converge before submission:
- the pane is in disconnected Cloud Mode state;
- it has an ambient task id in `AmbientAgentViewModel`;
- the next non-empty prompt is submitted through `TerminalView::try_submit_pending_cloud_followup`;
- `try_submit_pending_cloud_followup` calls `AmbientAgentViewModel::submit_cloud_followup`;
- the ambient model emits `FollowupDispatched` and later `FollowupSessionReady`;
- the deferred Cloud Mode manager attaches the fresh shared session through `attach_followup_session`.
`Input::submit_ai_query` should no longer be the mechanism that routes follow-up prompts through `submit_viewer_ai_query` and `InputEvent::SendAgentPrompt`. That path is still appropriate for a true live shared-session viewer sending a prompt to the sharer, but it is not the semantic model for starting a new cloud follow-up execution.
### Stop using ambient transcript-viewer status as an ambient identity source
Remove `ConversationTranscriptViewerStatus::ViewingAmbientConversation` from the new design. If the variant can be deleted cleanly, replace its call sites with Cloud Mode predicates:
- `TerminalModel::ambient_agent_task_id` should derive ambient identity from `shared_session_source_type` and/or the `AmbientAgentViewModel`, not from transcript-viewer status.
- `TerminalPane::snapshot` should snapshot restored ambient panes as `LeafContents::AmbientAgent`, because they are Cloud Mode panes.
- tab and pane-header ambient indicators should use `is_shared_ambient_agent_session`, `TerminalView::ambient_agent_view_model`, or another explicit Cloud Mode ambient predicate instead of checking transcript-viewer status.
- transcript share-link behavior should remain on true transcript viewers; restored ambient panes should use the Cloud Mode/shared-session share/open behavior available while idle between executions.
It is acceptable to keep `ConversationTranscriptViewerStatus::Loading` and `ViewingLocalConversation` for generic local/non-ambient transcript viewers. The key invariant is that ambient restored conversations do not become read-only because they are marked as transcript viewers.
### Keep generic transcript behavior as the fallback
Continue using the current `MockTerminalManager` transcript path when:
- `HandoffCloudCloud` is disabled;
- no effective task id exists;
- the conversation data is not cloud/ambient;
- the pane is opened on a platform or flow that cannot continue cloud runs.
This preserves `PRODUCT.md` invariants 22-24 and keeps the fix scoped to resumable cloud tasks.
### Align tombstone capability with pane capability
`ConversationEndedTombstoneView::new` currently infers cloud Continue eligibility from task id plus feature flag. Prefer adding a capability argument from `TerminalView::insert_conversation_ended_tombstone`, for example `CloudContinueCapability::Available(AmbientAgentTaskId)` vs `Unavailable`, so the button reflects whether the surrounding view can actually submit a cloud follow-up.
If restored ambient panes are always Cloud Mode panes with an ambient model, the first implementation can keep the existing constructor logic and add a regression test. The capability argument is safer because it prevents a stale task id on a generic transcript viewer from showing a cloud `Continue` action that can only fall back to the generic toast.
## Testing and validation
Map tests to `PRODUCT.md` behavior invariants instead of duplicating product requirements:
- Invariants 1-5 and 22-24: add PaneGroup/workspace tests that opening a completed cloud task with `HandoffCloudCloud` enabled creates a Cloud Mode pane with an `AmbientAgentViewModel` and no ambient transcript-viewer status, while disabled/no-task/local cases use the existing transcript path.
- Invariants 6-12 and 26-28: add terminal/shared-session tests that tombstone Continue in a restored ambient Cloud Mode pane sets `pending_cloud_followup_task_id`, focuses input, submits through `AmbientAgentViewModel`, and does not hit the generic toast path.
- Add an input/terminal test that a disconnected Cloud Mode follow-up prompt emits the dedicated Cloud follow-up event before generic shared-session viewer gating. Cover both restored panes and panes whose initial shared session just ended so they share the same submission path.
- Add a state test that fresh/restored/between-executions Cloud Mode panes with no live session use `SharedSessionStatus::NotShared`, then transition to `ViewPending` only when `connect_session` or `attach_followup_session` starts joining a real session.
- Invariants 13-17 and 25: add tests that the view remains registered for the same ambient task, repeated `FollowupSessionReady` attaches through the deferred manager, and follow-up completion returns the pane to a between-executions Cloud Mode tombstone state.
- Invariants 18-21: extend existing follow-up error tests to cover transcript-originated follow-ups, including prompt restoration before request acceptance and Cloud Mode error state after accepted-but-failed startup.
Targeted checks:
- `cargo nextest run -p warp terminal::view::shared_session::view_impl_test terminal::view_test`
- `cargo nextest run -p warp pane_group::mod_tests ai::agent_conversations_model_tests`
- `cargo check -p warp --features handoff_cloud_cloud`
Use the repo-standard formatting command when preparing the PR; do not run `cargo fmt --all` or file-specific `cargo fmt`.
## Risks and mitigations
### Duplicate transcript restoration
Creating a Cloud Mode view and then loading historical blocks could duplicate content if a follow-up session replays prior blocks. Keep follow-up joins on `SharedSessionInitialLoadMode::AppendFollowupScrollback` and rely on the existing block-id dedupe in `TerminalModel::append_followup_shared_session_scrollback`.
### Completed Cloud Mode state does not currently have a precise model status
`AmbientAgentViewModel::enter_viewing_existing_session` currently sets `Status::AgentRunning`, which may not accurately describe a completed task with no live session. If this causes incorrect setup/progress/footer behavior, add a between-executions/viewing-existing status instead of falling back to transcript-viewer state.
### Read-only transcript state blocks input
`TerminalModel::is_read_only` returns true when `conversation_transcript_viewer_status` is set in `app/src/terminal/model/terminal_model.rs (1534-1554)`. Restored ambient panes should avoid setting transcript-viewer status entirely so cloud follow-up input is not blocked. Generic transcript viewers can keep the existing read-only behavior.
### Manager/view mismatch
Retrofitting a `MockTerminalManager` transcript viewer with cloud follow-up subscriptions would duplicate Cloud Mode setup logic. Replace the loading pane with a deferred shared-session viewer manager for restored ambient conversations instead.
### Authorization and stale task state
The client may think a task is resumable when the server rejects follow-up creation or returns stale no-execution data. Treat the follow-up API as authoritative and surface errors through existing Cloud Mode error/auth/capacity/credits states.
## Parallelization
This fix is small enough to implement sequentially because the key change is one coherent pane-construction path. If split, one agent can own workspace/pane-group creation and restore behavior while another owns tombstone/input/read-only tests. They converge at the restored ambient Cloud Mode pane helper and should avoid simultaneous edits to the same `PaneGroup` creation functions.