Files
galaxy/specs/REMOTE-1573/TECH.md
T

95 lines
8.6 KiB
Markdown

# 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.