first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# REMOTE-1573 — Settings to gate local-to-cloud handoff and snapshot upload
|
||||
|
||||
## Summary
|
||||
|
||||
Add user-facing settings to control local-to-cloud handoff, gate the `&` prefix entrypoint independently, and ensure snapshot uploads are disabled when cloud conversations are off.
|
||||
|
||||
Figma: none provided
|
||||
|
||||
## Behavior
|
||||
|
||||
### Setting: handoff enabled
|
||||
|
||||
1. A new boolean setting "Cloud handoff" appears in the AI settings page under the Cloud Agents section. It defaults to **enabled**.
|
||||
|
||||
2. When the setting is enabled, all local-to-cloud handoff surfaces are available: the `&` compose prefix, the `/handoff` slash command, and the "Handoff to cloud" footer chip in agent view.
|
||||
|
||||
3. When the setting is disabled, all three surfaces are hidden or inert:
|
||||
- Typing `&` as the first character in agent input does **not** enter cloud handoff compose mode.
|
||||
- The `/handoff` slash command is not shown in the slash command menu and returns `false` (no-op) if invoked programmatically.
|
||||
- The "Handoff to cloud" footer chip is not rendered.
|
||||
- `WorkspaceAction::OpenLocalToCloudHandoffPane` dispatches are no-ops.
|
||||
|
||||
4. The setting is persisted locally and synced to cloud via the existing `define_settings_group!` / Warp Drive settings infrastructure, following the pattern of other AI settings (e.g. `orchestration_enabled`). TOML path: `agents.warp_agent.other.cloud_handoff_enabled`.
|
||||
|
||||
### Setting: `&` prefix entrypoint enabled
|
||||
|
||||
5. A second boolean setting "Use & to trigger handoff" appears as a sub-setting beneath the handoff toggle. It defaults to **enabled**.
|
||||
|
||||
6. When this sub-setting is enabled and the parent handoff setting is also enabled, typing `&` as the first character in a local agent conversation's input activates cloud handoff compose mode (existing behavior).
|
||||
|
||||
7. When the sub-setting is disabled (but the parent is enabled), the `&` prefix no longer triggers handoff compose mode. The `/handoff` slash command and footer chip remain available — only the keyboard shortcut is suppressed.
|
||||
|
||||
8. When the parent handoff setting is disabled, the `&` sub-setting row is hidden entirely. Its stored value is preserved so re-enabling the parent restores the user's previous choice.
|
||||
|
||||
### Force-disabling handoff when prerequisites are missing
|
||||
|
||||
9. The handoff setting is force-disabled (toggle rendered checked-off and non-interactive) when **either** of these conditions is true:
|
||||
- The user's cloud conversation storage setting is off (user-level `is_cloud_conversation_storage_enabled == false`, or org-level `cloud_conversation_storage_settings == Disable`).
|
||||
- The user or org has AI disabled.
|
||||
|
||||
10. When force-disabled, the toggle shows a tooltip explaining why: "Cloud handoff requires cloud conversations to be enabled."
|
||||
|
||||
11. When force-disabled, the effective value of the setting is `false` regardless of the stored value. All handoff surfaces (invariants 3a–3d) are suppressed.
|
||||
|
||||
12. If the prerequisites become satisfied again (user re-enables cloud conversations), the toggle becomes interactive and the stored value takes effect again.
|
||||
|
||||
### Snapshot gating: local-to-cloud handoff
|
||||
|
||||
13. When the effective handoff setting is disabled (either by user choice or force-disabled), no local-to-cloud handoff flow runs, so `fork_conversation` and `upload_snapshot_for_handoff` are never called. This is a natural consequence of invariant 3, not a separate gate.
|
||||
|
||||
14. Independently of the handoff setting, if cloud conversation storage is disabled at the time a cloud agent is spawned (from any surface — cloud mode compose, handoff, etc.), the client sets `snapshot_disabled: true` on the `SpawnAgentRequest` so the cloud agent's end-of-run snapshot upload is also skipped.
|
||||
|
||||
15. The `snapshot_disabled` field is added to `SpawnAgentRequest` as an optional boolean. When `None` or absent, the server/agent uses its default behavior (snapshot enabled). When `Some(true)`, the cloud agent skips the end-of-run snapshot upload pipeline.
|
||||
|
||||
### Snapshot gating: all cloud agent spawns
|
||||
|
||||
16. The `snapshot_disabled` flag is set on **every** cloud agent spawn from the client (not just handoff spawns) when cloud conversation storage is disabled. This includes spawns from cloud mode compose, the agent management view, and any other client-initiated spawn path that goes through `AmbientAgentViewModel::spawn_agent` or `spawn_agent_with_request`.
|
||||
|
||||
17. Cloud-to-cloud follow-up submissions (`submit_cloud_followup`) are **not** gated — follow-ups are allowed regardless of cloud conversation storage, since the parent cloud run already has a conversation. However, the `snapshot_disabled` flag propagates to follow-up runs the same way it does for initial spawns.
|
||||
|
||||
### Interaction with cloud-to-cloud handoff
|
||||
|
||||
18. The handoff setting gates local-to-cloud handoff only. Cloud-to-cloud follow-ups (the "Continue" tombstone flow gated by `HandoffCloudCloud`) are unaffected by this setting.
|
||||
|
||||
### Edge cases
|
||||
|
||||
19. If the user disables cloud conversation storage while a local-to-cloud handoff is in progress (fork + snapshot upload already in flight), the in-flight operation completes. The setting change takes effect on the next handoff attempt.
|
||||
|
||||
20. The setting does not affect the SDK/CLI `oz` agent path.
|
||||
|
||||
21. Anonymous and logged-out users never see the handoff setting (AI settings are hidden when AI is disabled, and AI is disabled for anonymous/logged-out users).
|
||||
@@ -0,0 +1,94 @@
|
||||
# REMOTE-1573 — Tech spec
|
||||
|
||||
## Context
|
||||
|
||||
This spec implements the behavior described in `specs/REMOTE-1573/PRODUCT.md`. The work touches five areas:
|
||||
|
||||
1. **New AI settings** — two booleans (`cloud_handoff_enabled`, `ampersand_handoff_enabled`) in the `AISettings` settings group (`app/src/settings/ai.rs:710-1463`).
|
||||
2. **Settings UI** — a new widget section on the AI settings page (`app/src/settings_view/ai_page.rs`) following the pattern of `CloudAgentComputerUseWidget` (line 6191).
|
||||
3. **Handoff surface gating** — using the effective setting value to gate the `&` prefix (`app/src/terminal/input.rs:3721-3751`), the `/handoff` slash command (`app/src/terminal/input/slash_commands/mod.rs:882-910`), the footer chip (`app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:2043-2053`), and the workspace action handler (`app/src/workspace/view.rs:20681-20691`).
|
||||
4. **Snapshot gating** — adding `snapshot_disabled` to `SpawnAgentRequest` and setting it from the cloud-conversation-storage state in every client-side spawn path (`app/src/terminal/view/ambient_agent/model.rs:967-994`, `build_handoff_spawn_request` at line 569).
|
||||
|
||||
### How the effective handoff value is derived
|
||||
|
||||
The effective value is `false` when any of these is true (PRODUCT.md invariant 9):
|
||||
- `AISettings::is_any_ai_enabled()` returns `false`
|
||||
- Cloud conversation storage is effectively disabled (user-level `is_cloud_conversation_storage_enabled == false` on `PrivacySettings`, or org-level `cloud_conversation_storage_settings == Disable` on `WorkspaceSettings`)
|
||||
- Feature flags `OzHandoff` or `HandoffLocalCloud` are off (existing gate in `is_local_to_cloud_handoff_available()` at `app/src/ai/blocklist/mod.rs:11-16`)
|
||||
- The user has toggled the setting off
|
||||
|
||||
This should be computed as a single helper function so all four gating sites share the same logic.
|
||||
|
||||
### How `snapshot_disabled` propagates
|
||||
|
||||
`SpawnAgentRequest` is serialized to JSON and sent to `POST /agent/run`. The server stores it on the queued execution input. The cloud agent reads it from task metadata in `AgentSDK::fetch_secrets_and_attachments` (`app/src/ai/agent_sdk/mod.rs:957`) and wires it into `AgentDriverOptions.snapshot_disabled`. The existing `run_snapshot_upload` (`app/src/ai/agent_sdk/driver.rs:2368-2427`) already respects this field. So the only client-side work is adding the field to `SpawnAgentRequest` and setting it before spawn.
|
||||
|
||||
We pass `snapshot_disabled` through the spawn request rather than checking `PrivacySettings` directly in the driver because the driver never reads user-configurable settings singletons (`PrivacySettings`, `AISettings`, `UserWorkspaces`). All its configuration flows in through `AgentDriverOptions`, which is populated from CLI args and server task metadata. The only singletons the driver accesses are `FeatureFlag` (compile-time/runtime flags) and `ServerApiProvider` (for API calls). Passing `snapshot_disabled` on the request keeps the driver's existing input-driven pattern intact.
|
||||
|
||||
## Proposed changes
|
||||
|
||||
### 1. Add settings to `AISettings`
|
||||
|
||||
**`app/src/settings/ai.rs`**: Add two new entries to `define_settings_group!(AISettings, ...)`, following the pattern of `orchestration_enabled`:
|
||||
|
||||
- `cloud_handoff_enabled: CloudHandoffEnabled` — `bool`, default `true`, TOML path `agents.warp_agent.other.cloud_handoff_enabled`, desktop-only, cloud-synced.
|
||||
- `ampersand_handoff_enabled: AmpersandHandoffEnabled` — `bool`, default `true`, TOML path `agents.warp_agent.other.ampersand_handoff_enabled`, desktop-only, cloud-synced.
|
||||
|
||||
Add two derived helpers on `AISettings`:
|
||||
|
||||
- `is_cloud_handoff_enabled(&self, app) -> bool` — returns `false` when any prerequisite is missing: AI disabled, setting off, feature flags off (delegates to `is_local_to_cloud_handoff_available()`), or cloud conversation storage off (user-level via `PrivacySettings` or org-level via `AdminEnablementSetting::Disable`).
|
||||
- `is_ampersand_handoff_enabled(&self, app) -> bool` — returns `is_cloud_handoff_enabled(app) && *self.ampersand_handoff_enabled`.
|
||||
|
||||
### 2. Add `snapshot_disabled` to `SpawnAgentRequest`
|
||||
|
||||
inside `SpawnAgentRequest`: Add an `Option<bool>` field `snapshot_disabled` after `initial_snapshot_token`, with `skip_serializing_if = "Option::is_none"`.
|
||||
|
||||
### 3. Set `snapshot_disabled` at spawn time
|
||||
|
||||
**`app/src/terminal/view/ambient_agent/model.rs`**: In `build_default_spawn_config`, after computing `computer_use_enabled`, read cloud conversation storage state and set `snapshot_disabled` on the returned request. Since `AgentConfigSnapshot` doesn't carry `snapshot_disabled` (it's a `SpawnAgentRequest`-level field), the flag must be set in `spawn_agent` and `build_handoff_spawn_request` directly.
|
||||
|
||||
In `spawn_agent` (~line 968) and `build_handoff_spawn_request` (~line 569), set `snapshot_disabled: should_disable_snapshot(ctx).then_some(true)` on the `SpawnAgentRequest`.
|
||||
|
||||
Add a private helper `should_disable_snapshot(ctx: &AppContext) -> bool` that returns `true` when cloud conversation storage is off — either user-level (`PrivacySettings::is_cloud_conversation_storage_enabled == false`) or org-level (`AdminEnablementSetting::Disable`).
|
||||
|
||||
### 4. Gate handoff surfaces
|
||||
|
||||
All four sites switch from the current `is_local_to_cloud_handoff_available()` check to the new `AISettings::is_cloud_handoff_enabled(app)`:
|
||||
|
||||
- **`&` prefix** (`app/src/terminal/input.rs:3721-3733`): `can_activate_cloud_handoff_prefix` — replace the `is_local_to_cloud_handoff_available()` call with `AISettings::as_ref(ctx).is_ampersand_handoff_enabled(ctx)`.
|
||||
- **`/handoff` slash command** (`app/src/terminal/input/slash_commands/mod.rs:882-887`): Replace the feature-flag check with `AISettings::as_ref(ctx).is_cloud_handoff_enabled(ctx)`.
|
||||
- **Footer chip** (`app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs:2043-2053`): Replace `is_local_to_cloud_handoff_available()` with `AISettings::as_ref(app).is_cloud_handoff_enabled(app)`.
|
||||
- **Workspace action** (`app/src/workspace/view.rs:13228`): `start_local_to_cloud_handoff` — replace the feature-flag check with the setting check. This is the final safety net.
|
||||
|
||||
`is_local_to_cloud_handoff_available()` in `app/src/ai/blocklist/mod.rs` is retained as a pure feature-flag check and called internally by `is_cloud_handoff_enabled`. Existing callers are migrated to the new setting-aware helper.
|
||||
|
||||
### 5. Settings UI widget
|
||||
|
||||
**`app/src/settings_view/ai_page.rs`**: Add a new `CloudHandoffWidget` struct implementing `SettingsWidget`, placed in the "Experimental" section near `CloudAgentComputerUseWidget`. Pattern:
|
||||
|
||||
- `should_render`: return `true` when `OzHandoff` and `HandoffLocalCloud` flags are on. The widget handles the disabled state internally.
|
||||
- `render`: Compute force-disabled state from cloud-conversation-storage. When force-disabled, render the toggle as disabled with a tooltip. Beneath the parent toggle, conditionally render the `&` sub-toggle only when the effective handoff value is true (hidden when parent is off).
|
||||
|
||||
Follow the exact pattern of `AgentAttributionWidget` (line 6093) for org-forced-disabled toggles with tooltips, and `CloudAgentComputerUseWidget` (line 6191) for the overall section layout.
|
||||
|
||||
## Testing and validation
|
||||
|
||||
**Unit tests** (new file `app/src/settings/ai_tests.rs` or inline `#[cfg(test)]` block):
|
||||
- `is_cloud_handoff_enabled` returns `false` when AI disabled, cloud convos off, or setting toggled off. Covers PRODUCT.md invariants 3, 9, 11.
|
||||
- `is_ampersand_handoff_enabled` returns `false` when parent is off or sub-setting is off. Covers invariants 6, 7, 8.
|
||||
- `should_disable_snapshot` returns `true` when cloud conversation storage is user-disabled or org-disabled. Covers invariants 16, 18.
|
||||
|
||||
**Existing tests**:
|
||||
- `app/src/terminal/input_tests.rs` already has `&`-prefix tests (line 5733+). Add a case where `cloud_handoff_enabled` is false and verify `&` does not activate handoff compose.
|
||||
|
||||
**Manual validation**:
|
||||
- Toggle handoff off in settings → verify `&`, `/handoff`, and footer chip are all suppressed.
|
||||
- Disable cloud conversation storage → verify handoff toggle becomes force-disabled with tooltip.
|
||||
- Spawn a cloud agent with cloud convos off → verify `snapshot_disabled: true` appears in the spawn request (network log or `log::info` in `spawn_agent`).
|
||||
- Re-enable cloud convos → verify handoff toggle becomes interactive again.
|
||||
|
||||
**Compilation**: `cargo check -p warp` and `cargo clippy --workspace --all-targets --all-features --tests -- -D warnings`.
|
||||
|
||||
## Parallelization
|
||||
|
||||
This task is not large enough to benefit from parallel sub-agents. The changes are tightly coupled (the setting definition feeds the UI widget, the gating sites, and the snapshot flag), and the total scope is ~200-300 LOC across ~8 files. Sequential implementation is the right approach.
|
||||
Reference in New Issue
Block a user