Files
galaxy/specs/REMOTE-1499/PRODUCT.md
T

12 KiB

Empty-Prompt Local-to-Cloud Handoff (REMOTE-1499) — Product Spec

Canonical product spec for the empty-prompt local-to-cloud handoff feature; describes the full end-to-end behavior across every stage of implementation. Per-stage sub-tech-specs cover what each stage delivers: STAGE-1.md (on harry/empty-prompt-handoff-wire-contract) and STAGE-2.md (on the stacked harry/empty-prompt-handoff-local). This is the single source of truth for the feature shape and is not modified by subsequent stages. Sibling specs (cross-repo):

  • warp-server/specs/REMOTE-1499/PRODUCT.md, warp-server/specs/REMOTE-1499/TECH.md (server-side counterparts, mirror this doc).

Problem

Today, every local-to-cloud handoff entry point requires a non-empty prompt to start a cloud run. The "Hand off to cloud" chip only enters compose mode, & + Enter on an empty buffer is a no-op, and /handoff with no argument is a no-op. Users who want to continue an in-progress local agent run in the cloud, or rehydrate workspace changes into a fresh cloud agent, have to type a throwaway prompt first.

Goals

  1. Three functionally equivalent entry points launch an immediate local→cloud handoff with no compose step, provided the active source conversation has at least one exchange (the client-side guardrail described below):
    • Click the "Hand off to cloud" chip on the agent input footer.
    • Type & + Enter on an empty buffer.
    • Type /handoff with no argument. All three funnel through the same start_local_to_cloud_handoff path in app/src/workspace/view.rs:13652-13663 and produce the same wire payload.
  2. Empty-prompt handoff against an in-progress local agent source: the client substitutes "Continue" on the wire (local→cloud only) so the cloud agent picks up the conversation context coherently. The same string is shown verbatim in the queued-prompt indicator block — wire and display are coupled by design.
  3. Empty-prompt handoff with snapshotted workspace changes: the client substitutes "Apply the workspace changes from my previous session." on the wire alongside the snapshot token. The cloud agent's first user-role turn carries that intent and the snapshot rehydration runs as before. The same string is shown verbatim in the queued-prompt indicator block.
  4. The cloud pane's "setting up…" Cloud Mode Setup V2 UI transitions out properly after environment setup, even when the cloud agent does not fire a first LLM exchange. The viewer learns the setup phase has ended via a new shared-session-protocol marker rather than depending on a first AppendedExchange event.
  5. Ships unconditionally on Stage 2 — no new feature flag. The entry points remain gated by OzHandoff && HandoffLocalCloud and the source-content guardrail.

Non-goals

  • Cloud→cloud empty Continue submission (deferred follow-up; out of scope here — see "Deferred follow-ups" below).
  • Removing the legacy AppendedExchange-driven setup-phase teardown fallback paths (deferred follow-up).

User-facing behavior

Entry points and the source-content guardrail

On a Warp client with OzHandoff && HandoffLocalCloud enabled, three entry points can launch an empty-prompt local→cloud handoff. Each gates the immediate-handoff path on a source-content guardrail: the source terminal view must have an active conversation (BlocklistAIHistoryModel::active_conversation(...) returns Some(...)) with at least one exchange. If the guardrail fails, the entry point falls back to pre-feature behavior:

  • Chip click. With an empty input buffer AND the guardrail passing, clicking the "Hand off to cloud" chip dispatches WorkspaceAction::OpenLocalToCloudHandoffPane { launch: None, environment_id: None, entry_point: HandoffEntryPoint::FooterChip } directly — a single click is a complete commit. With a non-empty input buffer (any source state), the chip instead enters & compose mode and preserves the in-flight prompt so the user can refine before submitting. Fallback (empty buffer, guardrail fails): activate & compose mode in the source input.
  • & + Enter on an empty buffer. When the guardrail passes, the empty-prompt early-return in maybe_launch_cloud_handoff_request is bypassed and the path dispatches OpenLocalToCloudHandoffPane { launch: None, ... } so the workspace synthesizer constructs the empty launch (with collected attachments). Entry point recorded as HandoffEntryPoint::Ampersand. Fallback (guardrail fails): no-op (swallow the Enter so the compose draft is preserved).
  • /handoff with no argument. Dispatches the same OpenLocalToCloudHandoffPane { launch: None, ... } as the chip when the user types /handoff with no following text and the guardrail passes. Entry point recorded as HandoffEntryPoint::SlashCommand. There is no separate handoff compose pane for this path — the dispatch is immediate. Fallback (guardrail fails): surface a "Nothing to hand off — start a conversation first." toast. All three entry points share one helper, crate::ai::blocklist::handoff::source_conversation_has_content, that evaluates the guardrail against self.terminal_view_id. When the guardrail passes, the three dispatches converge in start_local_to_cloud_handoff (app/src/workspace/view.rs), which synthesizes an empty PendingCloudLaunch (collecting attachments from the source input) and proceeds through the existing handoff machinery.

Wire-level substitution (display = wire)

At submit time, build_handoff_spawn_request in app/src/terminal/view/ambient_agent/model.rs decides the wire-level prompt client-side. The same string drives the queued-prompt indicator display — wire and display are coupled by design.

  • In-progress source + non-empty snapshot token. Empty user prompt + in-progress / blocked source (pending_handoff.source_conversation_active == true) + non-empty InitialSnapshotToken: substitute prompt: Some("Continue. Apply the workspace changes from my previous session.") on the wire. The cloud agent's first LLM turn both picks up the in-flight intent and rehydrates the workspace.
  • In-progress source only. Empty user prompt + in-progress source, no snapshot content: substitute prompt: Some("Continue") on the wire.
  • Idle source + non-empty snapshot token. Empty user prompt + idle source + non-empty InitialSnapshotToken: substitute prompt: Some("Apply the workspace changes from my previous session.") on the wire. The snapshot token still rides alongside.
  • Idle source + no snapshot token. Empty user prompt + idle source + no snapshot token: send prompt: None on the wire. The worker derives --skip-initial-turn from the execution input and the cloud agent skips its initial LLM turn.
  • Non-empty user prompt. The user's prompt flows through unchanged. All substitutions are local-to-cloud-only; the server never sees an in-progress or idle signal it has to interpret.

Queued-prompt indicator

The queued-prompt indicator is the small block that appears in the cloud agent's pane during the Cloud Mode Setup V2 warmup phase. It renders whatever string is on the wire: "Continue", "Apply the workspace changes from my previous session.", the user's typed prompt, or nothing (when the wire prompt is None). There is no separate label-selection enum; display tracks the wire one-to-one and the existing if !prompt.is_empty() guard in app/src/terminal/view/ambient_agent/view_impl.rs:154-189 suppresses the block when the wire prompt is None.

Cloud Mode Setup V2 teardown

The canonical signal that a cloud agent run's setup phase has completed is the CloudModeSetupPhaseEnded shared-session-protocol marker. Every cloud agent run (skip-initial-turn or normal) emits the marker once the environment setup phase finishes; the viewer receives the marker and transitions the pane out of the "setting up…" UI (flip the executing-startup-commands flag off, finish the setup command group, hide the group). This works for empty-prompt handoffs that skip the first LLM turn — no first AppendedExchange event is required to drive the teardown. For compatibility with viewers that connect to sharers running pre-feature builds, BlocklistAIHistoryEvent::AppendedExchange-driven fallback teardowns at app/src/terminal/view.rs:5496-5507 and app/src/terminal/view/ambient_agent/block/setup_command_text.rs:119-136 remain in place. Both teardowns are idempotent, so a new sharer + new viewer pair triggering both is harmless. Removal is tracked under "Deferred follow-ups".

Telemetry

  • CloudAgentTelemetryEvent::HandoffInitiated is extended with two new fields:
    • empty_prompt: bool — true when the user's submitted prompt was empty.
    • injection_path: HandoffInjectionPath { None | Continue | SnapshotRehydration } — which substitution path was intended at handoff initiation (mirrors the wire substitution decision).
  • New CloudAgentTelemetryEvent::HandoffSnapshotPrepared { derived_workspace_had_content: bool } fires after derive_touched_workspace settles. Analytics can join this against HandoffInitiated.injection_path to learn whether SnapshotRehydration paths actually carried snapshot content at submit time. The field reports what the snapshot pipeline produced; the upload itself may still fail downstream, so the wire prompt is not implied. Both events live under app/src/ai/ambient_agents/telemetry.rs. Their schemas are documented in the wire spec but no PII is added.

Feature flag

None. The client-side behavior changes ship unconditionally on Stage 2 — the three entry points are gated only by OzHandoff && HandoffLocalCloud and the source-content guardrail. Server-side validators (warp-server-4) accept empty prompts unconditionally when paired with a ConversationID. Because the client always carries a non-empty substituted prompt when a snapshot is present, the server no longer needs separate InitialSnapshotToken or rehydration-metadata branches to accept empty prompts.

Cross-repo dependencies

  • Server-side relaxations (warp-server-4) widen the validators on POST /agent/runs, the multi-agent runtime first-turn interceptor, and ProcessFollowupForTask. They land in Stage 1 on the shared branch harry/empty-prompt-handoff-wire-contract. See warp-server-4/specs/REMOTE-1499/.
  • Worker-derived skip_initial_turn (warp-server-4 + oz-agent-worker) computes the "should the cloud agent skip its initial LLM turn?" decision fresh per dispatch from execution.Input.Prompt.is_empty(). With the client-side snapshot-rehydration substitution always carrying a non-empty prompt, the InitialSnapshotToken check in the server-side helper is redundant and removed. Wire shape: a top-level AdditionalOzArgs []string on TaskAssignmentMessage, populated with --skip-initial-turn for eligible executions. The CLI flag --skip-initial-turn is the worker→CLI contract.
  • CloudModeSetupPhaseEnded shared-session-protocol marker (session-sharing-protocol + session-sharing-server).

Deferred follow-ups

Not in scope for this feature but tracked here so we don't lose them:

  • Cloud→cloud empty Continue submission. Gated on HandoffCloudCloud. Would permit empty submission via try_submit_pending_cloud_followup and plumb Option<String> through the submit_cloud_followup callsite.
  • Drop the legacy AppendedExchange-driven teardowns at app/src/terminal/view.rs:5496-5507 and app/src/terminal/view/ambient_agent/block/setup_command_text.rs:119-136 once enough time has passed that new viewers no longer need to support old sharers.
  • Revert the testing-only Cargo.toml local-path swaps in Cargo.toml:248 (warp-4) and session-sharing-server/server/Cargo.toml:36-40 to real git = ..., rev = <merged SHA> after session-sharing-protocol PR merges.