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
+102
View File
@@ -0,0 +1,102 @@
# Local-to-Cloud Handoff: `&` Entrypoint — Product Spec
Linear: [REMOTE-1558](https://linear.app/warpdotdev/issue/REMOTE-1558)
## Summary
Add a fast `&` input entrypoint for local agent users to start a cloud run from their current flow. Typing `&` followed by a prompt lets the user choose a cloud environment from the local agent footer, then pressing Enter either hands off the current local conversation to cloud or starts a fresh cloud run when there is no local conversation history.
## Problem
The existing local-to-cloud handoff flow is discoverable through the footer chip and `/handoff`, but it is not optimized for keyboard-first use. Users who already know they want the next prompt to run in the cloud need a prefix-style flow that feels as lightweight as `!` shell-mode input while preserving handoff context, environment selection, attachments, and error safety.
## Goals
- A user in a local fullscreen agent view can type `& query` to start a cloud run without manually opening a handoff pane and pressing Enter again.
- The `&` flow exposes environment selection before submission by reusing the same visual and interaction patterns as the existing cloud environment selector.
- `/handoff query` starts the same auto-run behavior as `& query`; `/handoff` without a query activates `&` handoff-compose mode, same as the footer chip.
- While `&` mode is active, the input is explicitly locked in AI mode so autodetection and shell-mode transitions cannot steal the prompt.
- Auto-run submissions feel instant: once Warp opens and claims the cloud launch surface, the prompt leaves the editable input and the cloud pane shows a queued/starting state immediately, even if local handoff preparation is still finishing.
- The feature never silently drops the user's prompt or pending file/image attachments when handoff preparation fails.
- Existing local-to-cloud handoff behavior remains available for users who want to review or edit before submitting.
## Non-goals
- Redesigning the environment selector, environment management, or cloud-mode input UI.
- Adding an environment argument syntax to `/handoff`.
- Adding new selected text/block/document context serialization for cloud-mode submit. `&` matches current cloud-mode support by carrying prompt text and pending file/image attachments.
- Changing cloud-to-cloud handoff behavior.
- Bidirectional sync after handoff. Local and cloud conversations still diverge after the handoff point.
- Making pasted `& query` activate handoff mode. The prefix is an explicit typed shortcut.
## Figma
Figma: none provided.
## Behavior
### `&` handoff-compose mode
1. When the user is in a local fullscreen agent view and types `&` as the very first character of an empty input, the input enters handoff-compose mode.
2. `&` only activates handoff-compose mode when it is typed by the user as the first character. It does not activate when:
- The user pastes text beginning with `&`.
- The input has leading whitespace before `&`.
- `&` appears anywhere other than the first character.
- The user is not in a local fullscreen agent view.
- The input is already in `!` shell mode.
3. After activation, the visible input behaves like `!` shell-mode input: the literal `&` is removed from the prompt text and rendered as a visible input indicator/chip. The prompt content sent to the cloud does not include the leading `&`. The `&` indicator uses the Agent/AI magenta color, not shell blue.
4. While handoff-compose mode is active, the input is locked in AI mode. Autodetection does not unlock it, and typed `!` is prompt text rather than a shell-mode transition until the user exits cloud handoff mode.
5. While handoff-compose mode is active and the prompt is empty, the input shows explanatory ghost/hint text so the user understands that the prompt will start a cloud run. Once the user has typed any prompt text, this ghost/hint text is hidden.
6. The exit affordance for handoff-compose mode mirrors `!` shell mode: the Agent View message bar shows Enter + "to hand off to cloud" and Backspace + "to dismiss". When the prompt is empty (Backspace can exit immediately), both labels use Agent/AI magenta. When prompt text is present, the Backspace label follows the same disabled/muted treatment as the `!` shell-mode message, while the Enter label stays active-colored.
7. When the prompt is empty, the user can press Backspace to exit cloud handoff mode. Exiting this way removes the `&` indicator and hides the transient environment selector.
8. While handoff-compose mode is active, the agent footer shows a transient environment selector in the existing footer left-side chip area. The selector uses the same visual style, menu behavior, focus behavior, labels, and environment-management affordance as the existing cloud-mode environment selector.
9. The transient environment selector is shown only for `&` handoff-compose mode. It is not shown for ordinary local agent prompts or `/handoff query`.
10. If the user selects an environment from the transient selector, that selection applies to the next `&` submission and persists as the user's last selected cloud environment, matching the existing environment selector behavior.
11. If the user does not explicitly select an environment in the transient selector, submission uses the normal cloud environment defaulting behavior. For a non-empty handoff source, touched-repo overlap may choose a better default after handoff preparation; for an empty source, the run uses the saved/default environment if one exists.
12. An explicit user selection in the transient selector always wins over any touched-repo overlap default discovered later.
13. If no environment exists or no environment is selected, `& query` can still start a cloud run without an environment, matching normal cloud-mode behavior.
14. While in handoff-compose mode, editing the prompt down to empty does not exit the mode; the user remains in handoff-compose mode with the `&` indicator and transient environment selector visible. Exiting requires an explicit affordance: Backspace on an already-empty buffer (item 7), Escape (item 15), or a programmatic buffer clear (e.g. starting a new conversation).
15. Pressing Escape while in handoff-compose mode exits handoff-compose mode, removes the `&` indicator, hides the transient environment selector, and keeps the prompt text in the local input.
16. Normal prompt editing continues to work while in handoff-compose mode. Pending file/image attachments remain visible and editable until the prompt is submitted or the user removes them.
### Submitting `& query`
17. Pressing Enter in handoff-compose mode with a non-empty prompt starts the cloud path. The user is not required to press Enter again in the cloud pane.
18. After Warp opens and claims the destination cloud surface, auto-submit is optimistically queued immediately: the submitted prompt is owned by the pending launch state rather than hydrated into the destination editor, and the cloud pane moves into its queued/starting state before slower handoff preparation finishes.
19. The submitted cloud prompt includes:
- The prompt text without the leading `&`.
- Pending file/image attachments.
- The environment chosen in the transient selector, when the user chose one.
20. If the active local conversation is empty, `& query` starts a normal fresh cloud run:
- No local conversation fork is created.
- No local-to-cloud snapshot is prepared.
- The selected/default environment is used when available.
- The cloud pane opens through the normal cloud-mode flow.
21. If the active local conversation is non-empty, idle, and has synced cloud conversation identity, `& query` starts a local-to-cloud handoff:
- The cloud pane opens in the same pane stack as the source pane.
- The source local agent view exits behind the cloud pane, matching the current handoff navigation behavior.
- The cloud pane shows the source conversation history as the handoff context.
- The prompt is optimistically queued as soon as the handoff pane is ready to own it, without appearing as editable destination input; the actual cloud run dispatch occurs automatically once the conversation fork, environment selection, and local snapshot preparation are ready.
22. If the active local conversation is non-empty but currently running or blocked, submission is blocked. Warp shows a toast explaining that handoff can only start from an idle conversation, leaves the user in the source local agent view, and preserves the prompt and pending file/image attachments.
23. If the active local conversation is non-empty and idle but cannot be handed off because it lacks synced cloud conversation identity, submission is blocked. Warp shows a toast, leaves the user in the source local agent view, and preserves the prompt and pending file/image attachments.
24. If handoff preparation fails before a cloud pane exists, such as a failed conversation fork request, Warp stays in the source local agent view, shows a toast, and preserves the prompt and pending file/image attachments.
25. If handoff preparation fails after a cloud pane has opened but before the cloud run is accepted by the server, Warp restores the prompt and pending file/image attachments into the handoff pane, shows a toast, returns the pane to a retryable compose state, and lets the user retry manually from that pane.
26. Warp must not auto-start a non-empty local-to-cloud handoff without the intended local context. If the handoff cannot safely carry the forked conversation and usable prepared snapshot, it falls back to the retryable handoff pane state rather than silently starting a reduced-context cloud run.
27. Once the destination cloud surface claims an auto-submit launch, the source input no longer shows the submitted prompt or pending attachments. If the launch later fails before the server accepts the cloud run, the prompt and attachments are restored in the destination pane rather than the source input.
28. Closing a cloud pane while auto-start preparation is still in progress cancels that pending auto-start from the user's perspective. The source local conversation is unaffected.
### `/handoff`
29. `/handoff` with no query activates `&` handoff-compose mode on the local input, the same as clicking the footer chip or typing `&`. The user sees the `&` indicator, transient environment selector, and message bar hints, and must type a prompt and press Enter to proceed.
30. `/handoff query` starts the same auto-run behavior as `& query`, including immediate optimistic queueing after the destination cloud surface claims the launch.
31. `/handoff query` from an empty local agent conversation starts a normal fresh cloud run.
32. `/handoff query` from a non-empty local conversation attempts local-to-cloud handoff with automatic run start, using touched-repo overlap/default environment selection. It does not show the transient `&` environment selector before starting.
33. `/handoff query` follows the same blocked and failure behavior as `& query`: running/blocked conversations are blocked with a toast; missing synced cloud conversation identity is blocked with a toast; user input and pending file/image attachments are not silently discarded.
34. If `/handoff query` fails before a pane opens, the slash-command text or extracted prompt remains available in the source input so the user can retry or edit it. The exact text representation may follow existing slash-command input conventions, but the user's prompt must not be lost.
### Existing handoff chip
36. Clicking the "Hand off to cloud" footer chip activates `&` handoff-compose mode, matching the same state as typing `&` as the first character. If the user is already in handoff-compose mode, the chip click is a no-op.
37. The chip does not auto-start a cloud run or open a cloud pane. It only enters handoff-compose mode; the user must type a prompt and press Enter to proceed.
38. Because the chip activates `&` mode, the transient environment selector, message bar affordances, and input indicator all appear in the source local footer after clicking.
### Environment behavior
39. Environment selection has three priority levels for auto-started local-to-cloud handoff:
1. User's explicit transient `&` selection, if present.
2. Touched-repo overlap default, if a non-empty source conversation identifies one.
3. Existing cloud environment defaulting behavior.
40. Auto-started fresh cloud runs from empty conversations use only the explicit transient `&` selection or existing cloud environment defaults, because there is no touched local conversation to score for overlap.
41. Environment selector labels, disabled states, keyboard navigation, menu closing behavior, and "New environment" behavior should match the existing selector wherever the selector appears.
### Focus, keyboard, and accessibility
42. Activating `&` mode keeps focus in the prompt editor so the user can continue typing immediately.
43. Opening the transient environment selector follows the existing selector's focus and keyboard behavior. Closing it returns focus to the prompt editor.
44. The `&` indicator and the handoff-compose hint text are part of the input chrome and should be understandable to screen readers at least as well as the existing `!` input indicator and mode hints.
45. The visual `&` indicator and "backspace to exit cloud mode" affordance use Agent/AI magenta so they read as cloud-agent mode, not shell mode.
46. Submitting a successful `& query` moves focus to the opened cloud pane, matching the existing handoff/cloud-mode navigation behavior.
### Invariants
47. The prompt sent to the cloud never includes the leading `&`.
48. `&` handoff-compose mode is always locked AI input while active, and it never changes normal AI-vs-shell classification semantics outside the active prompt.
49. `&` handoff-compose mode and `!` shell mode are mutually exclusive. Activating one must prevent or exit the other; the input must never show both indicators or dispatch both mode behaviors for the same prompt.
50. The user's explicit environment choice is never overwritten by automatic touched-repo overlap.
51. User-entered prompt text and pending file/image attachments are never silently dropped on blocked or failed handoff attempts. If an optimistic auto-submit fails before the server accepts the cloud run, the prompt and attachments are restored into a retryable input.
52. Empty local conversations use fresh cloud-run behavior; non-empty eligible local conversations use local-to-cloud handoff behavior.
53. Local and cloud conversations remain independent after handoff. Continuing in one does not update the other.
+186
View File
@@ -0,0 +1,186 @@
# Local-to-Cloud Handoff: `&` Entrypoint — Tech Spec
Product spec: `specs/REMOTE-1558/PRODUCT.md`
Linear: [REMOTE-1558](https://linear.app/warpdotdev/issue/REMOTE-1558)
## Context
REMOTE-1558 adds a keyboard-first local-to-cloud launch path: local AgentView users type `& query`, optionally choose a cloud environment from the local footer, and press Enter once. The same auto-run path is also exposed through `/handoff query`; no-query `/handoff` and chip flows activate `&` handoff-compose mode.
Current code paths this builds on:
- `app/src/terminal/input.rs:495` defines the existing `!` shell-mode prefix. The typed-only prefix stripping and lock behavior live in `app/src/terminal/input.rs (9087-9200)`, empty-state escape/backspace handling lives in `app/src/terminal/input.rs (9686-9824)`, and the visible `!` indicator is rendered by `maybe_render_ai_input_indicators` in `app/src/terminal/input.rs (14701-14758)`.
- `InputType` is the semantic/classification mode for a buffer: `Shell` or `AI`. It is imported from `input_classifier` and maps directly into the session-sharing protocol's `Shell` / `AI` input mode in `app/src/ai/blocklist/input_model.rs (114-121)`. Do not add a `CloudHandoff` variant to `InputType`; `&` handoff compose is still an AI prompt semantically, with different destination and submit behavior.
- `!` is not stored as a standalone prefix enum today; it is represented by `BlocklistAIInputModel` being locked to `InputType::Shell`. `&` must therefore add explicit guards around the existing shell-lock transitions rather than assuming the two modes are naturally exclusive.
- `app/src/terminal/input.rs (11926-12211)` is the cloud-mode submit path. It collects pending image/file attachments into `AttachmentInput`, clears the editor and pending attachments, then calls either `AmbientAgentViewModel::spawn_agent` or `submit_handoff`. It does not serialize selected text/block/document context; `spawn_agent` and `submit_handoff` both set `referenced_attachments: vec![]` in `app/src/terminal/view/ambient_agent/model.rs (707-786)` and `app/src/terminal/view/ambient_agent/model.rs (1203-1258)`.
- `/handoff` is registered with an optional argument in `app/src/search/slash_command_menu/static_commands/commands.rs:174`. Slash parsing preserves the text after the first space in `app/src/terminal/input/slash_command_model.rs:416`, and the handler dispatches `WorkspaceAction::OpenLocalToCloudHandoffPane` for `/handoff query` and activates `&` handoff-compose mode for `/handoff` with no query.
- `WorkspaceAction::OpenLocalToCloudHandoffPane` is defined in `app/src/workspace/action.rs:489` and handled in `app/src/workspace/view.rs:20345`.
- `Workspace::start_local_to_cloud_handoff` in `app/src/workspace/view.rs (12894-12965)` currently requires an active non-empty conversation with a `server_conversation_token`; otherwise it toasts and opens no pane. `complete_local_to_cloud_handoff_open` in `app/src/workspace/view.rs (12967-13155)` materializes a local fork, pushes the cloud-mode pane, pre-fills an optional prompt, restores the forked conversation, binds the fork token, exits the source agent view, seeds `PendingHandoff`, and starts touched-workspace derivation plus snapshot upload.
- `PendingHandoff` and handoff readiness live on `AmbientAgentViewModel` in `app/src/terminal/view/ambient_agent/model.rs (78-138)` and `app/src/terminal/view/ambient_agent/model.rs (397-509)`. `submit_handoff` builds a normal `SpawnAgentRequest` with `conversation_id` set to the forked server conversation id and `initial_snapshot_token` set from the prepared upload.
- `EnvironmentSelector` in `app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs (129-480)` is currently hard-bound to `ModelHandle<AmbientAgentViewModel>`. It persists explicit selections to `CloudAgentSettings::last_selected_environment_id` and only enables while the ambient model is composing. The footer renders it only for ambient cloud panes in `app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs (2014-2039)`.
- `AgentMessageBar` in `app/src/ai/blocklist/agent_view/agent_message_bar.rs` already owns the shell-mode "backspace to exit shell mode" affordance through `ExitBashModeMessageProducer`; the cloud-prefix affordance should be added there rather than as unrelated input chrome.
- Agent/AI magenta is available through existing AI color helpers such as `ai_brand_color` in `app/src/ai/blocklist/view_util.rs`, and should be used for the `&` indicator and cloud-mode exit affordance instead of shell-mode blue.
- `Input::is_cloud_mode_input_v2_composing` already excludes local-to-cloud handoff panes in `app/src/terminal/input/agent.rs:65`, so the new handoff compose path should stay on the existing AgentView input UI.
## Proposed changes
### 1. Launch payload types
Two types in `app/src/ai/blocklist/handoff/mod.rs` carry the handoff payload from the source input through the workspace action into the cloud pane:
- `HandoffLaunchAttachments` — the pre-read `Vec<AttachmentInput>` for `SpawnAgentRequest`, plus a `Vec<PendingAttachment>` display/restoration snapshot so failure paths can repopulate the source input.
- `PendingCloudLaunch``prompt: String` and `attachments: HandoffLaunchAttachments`. Reused by the action variant and by `PendingHandoff.auto_submit`.
The workspace action carries these directly — no wrapper struct or request-id coordination. The source input eagerly clears its compose state on dispatch and workspace restores it on failure (see §4):
```rust path=null start=null
OpenLocalToCloudHandoffPane {
launch: Option<PendingCloudLaunch>,
explicit_environment_id: Option<SyncId>,
}
```
Do not add selected text/block/document serialization in this spec. `&` and `/handoff query` should match the current cloud-mode submit behavior: prompt plus pending image/file attachments.
### 2. Add input-local prefix state for `&` and `!`
Do not model `&` by adding a third `InputType`. `InputType` should remain a two-way semantic classification (`Shell` or `AI`) used by autodetection, slash-command gating, UDI controls, and session-sharing serialization. A cloud-handoff draft should continue to report as AI input anywhere that asks whether the buffer is an AI prompt.
Add an `Input`-local prefix-mode layer instead:
```rust path=null start=null
enum InputPrefixMode {
None,
Shell,
CloudHandoff,
}
```
`InputPrefixMode::Shell` should be derived from the existing locked-shell state, not separately stored. `InputPrefixMode::CloudHandoff` should be backed by an `Input`-owned `ModelHandle<HandoffComposeState>` rather than extending `BlocklistAIInputModel`. This keeps normal AI-vs-shell classification independent from the handoff launch draft while still giving prefix rendering and keyboard handling one mutually exclusive parent enum.
`HandoffComposeState` should track:
- whether handoff compose mode is active;
- the selected environment id, if any;
- whether the environment was explicitly selected by the user;
Activation should mirror the `!` path while defending mutual exclusion:
1. In `handle_editor_event`, recognize `&` only on `EditOrigin::UserTyped`, only when it is the first character of an otherwise empty buffer, and only when the current input is the fullscreen local AgentView input. Do not activate in terminal mode, cloud-mode panes, CLI-agent rich input, pasted input, buffers with leading whitespace, or locked shell mode.
2. If the visible `!` shell-mode indicator is active, typed `&` is normal shell text; the user must exit shell mode before entering handoff compose mode.
3. Strip the literal `&` from the editor buffer, set `HandoffComposeState.active = true`, set the input config directly to `InputConfig { input_type: InputType::AI, is_locked: true }`, keep focus in the editor, and notify the footer. Do not call `unlocked_if_autodetection_enabled` for this transition; cloud-prefix mode is intentionally locked AI until exit.
4. While `HandoffComposeState.active` is true, skip the existing `TERMINAL_INPUT_PREFIX` activation branch and any autodetection unlock path. Typed `!` is prompt text in the cloud-launch draft; it must not lock the input to shell mode until the user exits `&` mode.
5. Any other path that locks the input to shell mode while `&` is active, such as explicit terminal-mode actions or `DeleteAllLeft`, should call `exit_handoff_compose_mode` before applying the shell lock. Conversely, `&` activation should no-op if the shell lock is already active. This makes `HandoffComposeState.active` and locked-shell input a defended invariant rather than a rendering convention.
6. Add a helper like `current_prefix_mode(ctx) -> InputPrefixMode` and route activation, rendering, hint text, Escape/Backspace, Enter behavior, and tests through it. This helper should derive `Shell` from locked `InputType::Shell`, derive `CloudHandoff` from `HandoffComposeState.active`, and never let both be true from a caller's perspective.
7. Render `&` through `maybe_render_ai_input_indicators` as a sibling of the existing `!` indicator path. Defensively prefer `&` only if `HandoffComposeState.active`; otherwise render `!` from locked-shell state. The `&` indicator should use Agent/AI magenta (`ai_brand_color` or the equivalent theme magenta), not `ansi_fg_blue`. Tests should assert both indicators cannot render together.
8. In `set_zero_state_hint_text`, use the handoff hint only when `HandoffComposeState.active` and the editor buffer is empty. Keep this hint focused on what the prompt will do; the Backspace exit copy belongs to the message bar.
9. Thread the `HandoffComposeState` handle into the Agent View message bar path by passing it through `BlocklistAIStatusBar::new` and `AgentMessageBar::new`, or by passing a small derived prefix-mode provider if that keeps constructor churn lower. Add an `ExitCloudHandoffModeMessageProducer` parallel to `ExitBashModeMessageProducer` that renders Enter + "to hand off to cloud" and Backspace + "to dismiss". Both labels use Agent/AI magenta when the editor is empty; when prompt text is present, the Backspace label follows the shell-mode muted/disabled behavior while the Enter label stays active-colored.
10. On `BackspaceOnEmptyBuffer` / `BackspaceAtBeginningOfBuffer` or Escape, call a single `exit_handoff_compose_mode` helper that clears the handoff state, restores normal unlocked AI/autodetection behavior for future prompts, but preserves the prompt for Escape. Normal editing that empties the buffer does not exit the mode — the user must backspace on an already-empty buffer to exit, matching the `!` shell-mode two-step exit. Programmatic clears (e.g. `clear_buffer_and_reset_undo_stack`) also call the exit helper.
On Enter with a non-empty handoff prompt, build a `PendingCloudLaunch` from the current buffer, pre-read cloud-mode-supported image/file attachments using the same logic as `app/src/terminal/input.rs (11926-12131)`, read the explicit environment id from `HandoffComposeState`, then eagerly exit compose mode and clear the source buffer and pending attachments. Dispatch `OpenLocalToCloudHandoffPane { launch, explicit_environment_id }`. If workspace fails to open the target pane, it calls `Input::restore_cloud_handoff_draft` to re-activate compose mode, repopulate the buffer with the prompt, and restore the display attachments and environment selection.
### 3. Generalize `EnvironmentSelector`
Refactor `EnvironmentSelector` so it binds to a small environment-selection target interface instead of directly storing `ModelHandle<AmbientAgentViewModel>`.
The target interface needs:
- `selected_environment_id(ctx) -> Option<SyncId>`;
- `set_environment_id(environment_id, is_explicit, ctx)` — the `is_explicit` flag tracks whether the user made a deliberate selection (preventing touched-repo overlap from overwriting it later);
- `ensure_default_environment_id(environment_id, ctx)` — sets a default only when no selection exists yet, and on the `CloudPane` target also respects explicit environment locks from a pending handoff;
- `is_configuring(ctx) -> bool`;
- a change event or subscription hook for refreshing menu/button state.
Implement the target for `AmbientAgentViewModel` and for `HandoffComposeState`. Preserve existing selector behavior: selecting an environment from either target persists to `CloudAgentSettings::last_selected_environment_id`, opens the environment-management pane through the existing footer event, and uses the same label/menu/disabled states.
Update `AgentInputFooter` so it can receive the handoff state handle in addition to the optional ambient model. Render the selector in the left footer area when either:
- an ambient cloud pane is composing, preserving current behavior; or
- `HandoffComposeState.active` is true, producing the transient `&` selector.
The transient selector should not be part of the configurable toolbar item list; it is mode chrome like the `&` input indicator.
### 4. Refactor workspace entrypoints with eager-clear and restore-on-failure
The source input eagerly exits compose mode and clears its buffer/attachments before dispatching. The footer chip and `/handoff` with no query activate `&` handoff-compose mode directly. `/handoff query` builds a `PendingCloudLaunch`, clears the buffer normally with other slash commands, and dispatches. `& query` builds a `PendingCloudLaunch`, eagerly exits compose mode, and dispatches.
Workspace classifies the active source:
1. No active conversation or an empty active conversation: fresh cloud run/compose.
2. Non-empty conversation whose status is running or blocked: restore source input, toast.
3. Non-empty idle conversation without `server_conversation_token`: restore source input, toast.
4. Non-empty idle conversation with `server_conversation_token`: local-to-cloud handoff.
On any failure before the target pane is successfully opened (validation, fork RPC, pane creation), workspace calls `Input::restore_cloud_handoff_draft(launch, explicit_environment_id)` on the source view to re-activate compose mode, repopulate the buffer with the prompt, and restore display attachments and environment selection.
For the async fork path (`start_local_to_cloud_handoff`), the fork RPC callback's error branch calls `restore_cloud_handoff_draft` before showing the toast. The success branch proceeds to `complete_local_to_cloud_handoff_open` as today.
### 5. Fresh cloud run/compose path for empty conversations
Add a helper on `Workspace` or `TerminalView` that starts normal cloud mode from an `OpenLocalToCloudHandoffPane` dispatch.
When `launch` is `None`, reuse the existing cloud-mode open path. When `launch` is `Some`, create the normal cloud-mode pane, apply `explicit_environment_id` to its `AmbientAgentViewModel` before building the spawn config, then call `spawn_agent` with the collected prompt and attachments. This avoids simulating editor input and keeps fresh-cloud auto-run aligned with the model-level submit API.
If no environment was explicitly selected, rely on the existing selector/defaulting behavior. If no environment exists, let `build_default_spawn_config` send `environment_id: None`, matching current cloud-mode behavior.
### 6. Local-to-cloud auto-submit
Extend `PendingHandoff` with:
- `auto_submit: Option<PendingCloudLaunch>` containing prompt and attachments;
- `explicit_environment_id: Option<SyncId>` or an equivalent environment-source marker.
- a submission phase that can distinguish idle manual compose, optimistically queued auto-submit, and active server dispatch. This can be a field on `PendingHandoff`, a small enum on `AmbientAgentViewModel`, or an equivalent representation, but callers need to know whether the user's prompt is hidden because Warp has queued it.
`PendingCloudLaunch` should carry both:
- the spawn-ready prompt and `AttachmentInput`s; and
- a restoration draft containing the prompt plus pending image/file attachment display state for retry.
When `complete_local_to_cloud_handoff_open` creates the handoff pane:
1. Apply `explicit_environment_id` to the pane model immediately when present.
2. For `Compose`, prefill the handoff pane input and install the pending image/file attachment snapshot as today.
3. For `AutoSubmit`, do not hydrate the submitted prompt or pending attachment display into the destination editor before queueing. Instead, seed `PendingHandoff` with the spawn payload and restoration draft, and move the pane into the queued/starting visual state immediately. If preparation later fails before the server accepts the run, restore that saved draft into the destination editor for manual retry.
4. Source input was already cleared eagerly at dispatch time, so no claim step is needed.
5. Start touched-workspace derivation and snapshot upload as today.
When touched-workspace derivation finishes, keep the current overlap selection behavior only if there was no explicit environment id. This enforces the product priority: explicit `&` selection, then touched-repo overlap, then default.
Add a model method such as `queue_auto_submit_handoff(ctx)` and call it immediately after seeding `PendingHandoff` for an `AutoSubmit` launch. It should:
- set the submission phase to optimistically queued;
- put the cloud pane in the same queued/starting visual family used before the first cloud response, so users do not see an editable prompt sitting in an unqueued pane;
- retain the restoration draft internally until the server has accepted the run;
- not call the server yet if touched-workspace derivation or snapshot upload is still pending.
Move the readiness check into the model by adding or updating `maybe_auto_submit_handoff(ctx)`. Call it after each `PendingHandoff` mutation that can make readiness true. It should:
- require an optimistically queued auto-submit payload;
- require `is_handoff_ready_to_submit()`;
- take the spawn payload exactly once;
- transition from queued/preparing to active dispatch;
- call `submit_handoff(prompt, attachments.spawn_inputs, ctx)`.
If any step after the destination pane owns the launch but before the server accepts the run fails, restore the restoration draft into the handoff pane input, restore the pending image/file attachment display state, reset the submission phase to manual compose/retry, and show the existing failure toast. This includes snapshot upload failure and immediate spawn request failure. A subsequent manual Enter should retry snapshot upload for the same touched workspace before calling `submit_handoff`; otherwise the product's retryable failure state is not real.
Once the server accepts the cloud run, discard the restoration draft. Later cloud-run failures should follow normal cloud-agent failure behavior rather than repopulating the input.
### 7. Slash command clearing semantics
`/handoff query` builds a `PendingCloudLaunch` and dispatches the workspace action. The normal slash-command buffer clear proceeds — no deferred-clear flag is needed since workspace restores the source input on failure via `restore_cloud_handoff_draft`. `/handoff` with no query activates `&` compose mode and lets the normal slash-command buffer clear proceed (the buffer is cleared to make room for the compose draft).
### 8. End-to-end flow
```mermaid
sequenceDiagram
participant U as User
participant I as Source Input
participant W as Workspace
participant HP as Handoff Pane
participant M as AmbientAgentViewModel
participant API as Server API
U->>I: Type "& fix tests"
I->>I: Strip &, enter HandoffComposeState
U->>I: Optionally pick environment
U->>I: Enter
I->>I: Eagerly exit compose mode, clear buffer/attachments
I->>W: OpenLocalToCloudHandoffPane { launch, explicit_environment_id }
W->>W: Classify source conversation
alt empty source
W->>HP: Open normal cloud pane
W->>M: Apply explicit env, spawn_agent
else eligible non-empty source
W->>API: fork_conversation(source token)
API-->>W: forked_conversation_id
W->>HP: Push handoff pane, restore local fork
W->>M: set PendingHandoff(auto_submit + restore draft)
Note over HP: Auto-submit draft is not hydrated into editable input
W->>M: queue auto-submit, show starting
par background prep
W->>M: set touched workspace
W->>M: set snapshot upload result
end
M->>M: maybe_auto_submit_handoff when ready
M->>API: POST /agent/runs with conversation_id + snapshot token
alt prep or immediate spawn failure before server accepts run
M->>HP: Restore prompt/attachments for manual retry
end
else blocked or ineligible
W->>I: restore_cloud_handoff_draft(launch, env)
W-->>U: Toast
end
```
## Risks and mitigations
- **Input clears before workspace opens the target pane.** Source input eagerly clears on dispatch; workspace calls `restore_cloud_handoff_draft` on every synchronous failure path to repopulate the prompt, display attachments, and environment selection. The async fork path restores in the error callback. Tests should cover both `&` and `/handoff query` failure restoration.
- **Environment overlap overwrites explicit user choice.** Store explicit environment state on `PendingHandoff` and skip `pick_handoff_overlap_env` when it is present.
- **Auto-submit queueing hides the user's prompt before dispatch.** Carry both spawn-ready `AttachmentInput`s and a restoration snapshot. Keep the restoration draft until the server accepts the run, and restore it on any pre-acceptance failure.
- **Snapshot failure remains unretryable.** Current handoff readiness blocks submit after `SnapshotUploadStatus::Failed`; this feature should add a manual retry branch so the failure pane can recover.
- **Selected text/block/document context mismatch.** Current cloud-mode submit does not serialize this context. This spec deliberately avoids inventing a one-off path for `&`; general cloud selected-context support should be added once and reused by normal cloud mode, `&`, and `/handoff query`.
## Testing and validation
### Unit tests
- `app/src/terminal/input_test.rs`: add `&` prefix tests parallel to `run_input_mode_prefix_test` for typed-only activation, paste/system insert non-activation, first-character-only activation, stripping, indicator state, Backspace empty-state exit (two-step: editing to empty stays in mode, backspace on empty exits), Escape preserving prompt text, and input-mode toggle being disabled while in handoff compose. Cover local fullscreen AgentView only, and assert terminal mode, cloud-mode panes, and CLI-agent rich input do not activate.
- `app/src/terminal/input_test.rs`: verify `&` activation locks the input as AI even when autodetection is enabled, typed `!` remains prompt text while `&` mode is active, and exiting `&` restores normal AI/autodetection behavior for future prompts.
- `app/src/terminal/input_test.rs` or view-rendering coverage for `maybe_render_ai_input_indicators`: verify the `&` indicator uses Agent/AI magenta and does not use shell-mode blue.
- `app/src/terminal/input_test.rs`: verify Enter in handoff compose builds a `PendingCloudLaunch`, eagerly clears the source buffer and compose state, and dispatches `OpenLocalToCloudHandoffPane`. Verify `restore_cloud_handoff_draft` re-activates compose mode, repopulates the buffer, and restores display attachments and environment.
- `app/src/ai/blocklist/agent_view/agent_message_bar*`: verify `ExitCloudHandoffModeMessageProducer` renders Backspace + "to exit cloud mode", uses Agent/AI magenta when the buffer is empty, and follows the shell-mode disabled/muted behavior when the buffer is non-empty.
- `app/src/ai/blocklist/agent_view/agent_input_footer/*_test.rs` or existing footer tests in `app/src/terminal/input_test.rs`: verify the transient environment selector renders only while `HandoffComposeState.active`, selection updates the handoff state, and selection persists to `CloudAgentSettings::last_selected_environment_id`.
- `app/src/terminal/input/slash_command_model_tests.rs`: keep optional argument parsing coverage for `/handoff query`; add coverage for argument text containing spaces.
- `app/src/terminal/input/slash_commands/*`: test that `/handoff query` clears the buffer normally and dispatches `OpenLocalToCloudHandoffPane`; `/handoff` without query activates `&` compose mode.
- `app/src/terminal/view/ambient_agent/model.rs` tests: cover `PendingHandoff` auto-submit entering the optimistically queued phase immediately, firing exactly once when touched workspace and snapshot upload both settle, explicit env preventing overlap replacement, snapshot failure restoring the draft for manual retry, and immediate spawn request failure restoring the draft before server acceptance.
### Integration / manual
- Product behaviors 1-18: type `&`, observe stripped magenta visible indicator, locked AI mode, empty-state cloud hint, magenta Backspace message, transient selector, Backspace-on-empty/Escape exits, and Enter requiring a non-empty prompt.
- Product behaviors 20 and 30-31: from an empty local AgentView conversation, `& query` and `/handoff query` open normal cloud mode and auto-start a fresh run with the prompt and file/image attachments.
- Product behaviors 21 and 30-32: from an eligible non-empty local conversation, both auto-run entrypoints open the handoff pane, immediately show queued/starting without an editable prompt gap, and dispatch the cloud run once fork, overlap/default env selection, and snapshot upload are ready.
- Product behaviors 22-27 and 33-34: running/blocked, missing-token, fork-failure, snapshot-failure, and immediate spawn-failure cases preserve or restore the prompt and image/file attachments according to whether workspace claimed the launch.
- Product behavior 49: `&` and `!` are mutually exclusive in both directions. Verify `&` cannot activate from visible shell mode, typed `!` inside `&` mode remains prompt text, explicit terminal-mode actions exit `&`, and the input never renders both indicators.
- Product behaviors 39-41 and 50: explicit `&` environment wins over touched-repo overlap and persists as the saved cloud environment; slash-command auto-run still allows overlap/default selection.
### Validation commands
- Compile the touched Rust targets after implementation. Prefer the repo's normal Rust check command if available in existing docs/scripts; otherwise use the narrowest `cargo check`/Bazel equivalent that covers `app/src/terminal/input.rs`, `app/src/workspace/view.rs`, and `app/src/terminal/view/ambient_agent/model.rs`.
- Run the focused unit tests added above.
## Parallelization
The implementation touches tightly coupled input, footer, workspace, and ambient-model state. One agent should implement it sequentially to avoid conflicting edits around `Input` and `AgentInputFooter`; validation can be split afterward if compile/test time becomes the bottleneck.
## Follow-ups
- Add general cloud-mode serialization for selected text/block/document context and plumb it through `SpawnAgentRequest.referenced_attachments` or the appropriate server contract. Once that exists, `PendingCloudLaunch` should carry the same selected-context payload for normal cloud mode, `&`, and `/handoff query`.
- Consider dedicated telemetry for `&` activation, auto-submit success, and blocked/failure reasons if existing slash/cloud telemetry is insufficient for rollout analysis.