92 lines
16 KiB
Markdown
92 lines
16 KiB
Markdown
# 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.
|