Files
galaxy/specs/APP-4319/handoff-cloud-cloud-resumable-transcripts/TECH.md
T

92 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 actions 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 panes 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.