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
+64
View File
@@ -0,0 +1,64 @@
# REMOTE-1591: Environment creation flow for local-to-cloud handoff
## Summary
When a user enters `&` handoff-compose mode but has no cloud environments, they should be able to create one inline via a modal overlaying the main window. After creating the environment, the handoff auto-submits with the new environment. Dismissing the modal without creating leaves the input unchanged.
Figma: none provided
## Behavior
### Entry into `&` compose mode with no environments
1. Typing `&` in agent view activates handoff-compose mode regardless of whether the user has any environments. The `&` prefix indicator, message bar hints, and locked-AI input behavior all apply the same as when environments exist.
2. The ghost text / placeholder in the input reads **"Handoff to cloud"** (the generic fallback), since there is no environment name to display in the "Hand off to \<env\>" pattern.
### Opening the creation modal
3. When the user presses Enter while in `&` handoff-compose mode and no environments exist:
- If the input buffer is non-empty, the prompt text is preserved (held in `&` compose state).
- A modal dialog overlays the main window containing the `UpdateEnvironmentForm` in Create mode.
- The modal uses the same form as Settings → Environments → Create (name, description, GitHub repos, Docker image, setup commands), rendered without the settings-page header (back button / page title) and instead with a modal-style close button and title.
4. If the input buffer is empty and the user presses Enter with no environments, nothing happens.
### Modal behavior
5. The modal renders as a centered overlay with a dark background overlay, a close button, and an Esc keybinding hint — the same pattern as other modals in the app (e.g. tab config modal, `EnvironmentSetupModeSelector`). Clicking outside the dialog or pressing Escape closes it.
6. The form inside the modal is the existing `UpdateEnvironmentForm` in Create mode, with `show_header = false` and `should_handle_escape_from_editor = true`. The submit button renders at the bottom-right of the form body (the existing non-header layout).
7. Focus moves into the form's name field when the modal opens. Tab order cycles through form fields as it does in the settings page.
8. Pressing Escape anywhere in the form, or clicking outside the dialog, closes the modal. This is the "dismiss without creating" path — see (13).
### Successful environment creation
9. When the user fills out the form and submits (Create button or Enter on the focused submit button), the modal closes immediately.
10. The environment is created on the server via an online-only API call (`create_ambient_agent_environment_online`). The modal waits for the server to confirm creation and return a `ServerId` before proceeding — this avoids race conditions with unsynced client IDs.
11. Once the server confirms, the handoff auto-submits: the prompt and any pending attachments are sent to `WorkspaceAction::OpenLocalToCloudHandoffPane` with the new environment's `ServerId`, following the same path as a normal `& query` submit.
### Failure during server creation
12. If the server call fails (network error, validation failure, etc.), the error is logged and an error toast is shown ("Failed to create environment: \<error\>"). The `&` compose input is preserved so the user can retry.
### Dismissal without creating
13. If the user closes the modal without creating an environment (Escape, clicking outside, or the close button), the input returns to `&` handoff-compose mode unchanged — the prompt text, attachments, and chip state are all preserved exactly as they were before the modal opened.
14. The user can re-trigger the modal by pressing Enter again, or exit `&` mode entirely via Backspace-on-empty / Escape as usual.
### Interaction with existing environment flows
15. If the user already has one or more environments, Enter in `&` compose mode submits the handoff as it does today — the modal never opens when environments exist.
16. If the user creates their first environment through Settings → Environments while `&` compose mode is active, the environment chip should update reactively (via the existing `CloudModel` subscription) to show the new environment name. At that point, Enter submits the handoff normally without opening the modal.
17. The modal does not interfere with the "share with team" checkbox — it renders the same as in the settings page create flow. If the user is on a team, the option appears.
### Edge cases
18. If the user is not logged in or not authenticated, the form's GitHub auth flow (repo selection) works the same as in settings — the OAuth redirect opens in the browser and the form refreshes on app focus.
+137
View File
@@ -0,0 +1,137 @@
# REMOTE-1591: Tech spec — Environment creation modal for handoff
## Context
When a user enters `&` handoff-compose mode but has zero cloud environments, there is currently no way to create one without leaving the flow. See `PRODUCT.md` for detailed user-facing behavior.
### Relevant code
**Handoff compose flow (input layer)**
- `app/src/terminal/input.rs` — `maybe_launch_cloud_handoff_request()` (line ~3810) is the Enter handler for `&` compose mode. Currently returns `true` (consumed) when the prompt is empty, or collects attachments and dispatches `WorkspaceAction::OpenLocalToCloudHandoffPane`.
- `app/src/terminal/input/handoff_compose.rs` — `HandoffComposeState` model tracking `&` mode activation and selected environment.
**Environment selector chip**
- `app/src/ai/blocklist/agent_view/agent_input_footer/environment_selector.rs` — `refresh_button()` (line 437) sets the chip label. Falls back to `"New environment"` when no env is selected.
**Environment creation form**
- `app/src/settings_view/update_environment_form.rs` — `UpdateEnvironmentForm` view. Already supports modal-style use via `show_header` (line 332), `should_handle_escape_from_editor` (line 336), and `auth_source` (line 342). Emits `UpdateEnvironmentFormEvent::Created { environment, share_with_team }` on submit.
**Prior art: embedding the form in non-settings contexts**
- `app/src/terminal/view/ambient_agent/first_time_setup.rs` — `FirstTimeCloudAgentSetupView` wraps the form with `show_header=false`, `should_handle_escape_from_editor=true`, handles `Created` by calling `UpdateManager::create_ambient_agent_environment()`. This is the pattern to follow for environment creation logic.
**Prior art: modal overlay rendering**
- `app/src/settings_view/agent_assisted_environment_modal.rs` — `AgentAssistedEnvironmentModal` renders using `Dialog::new().with_close_button().with_child().build()` wrapped in `Dismiss::new().prevent_interaction_with_other_elements()`, inside a `Container` with `ColorU::new(0, 0, 0, 179)` background. Uses `show()`/`hide()` visibility toggle and emits `Cancelled`/`Confirmed` events. This is the rendering pattern to follow.
- `app/src/ui_components/dialog.rs` — `Dialog` component used by modal overlays. Provides title, close button, child content, and bottom row.
**Workspace handoff dispatch**
- `app/src/workspace/view.rs` — `start_local_to_cloud_handoff()` (line 12972) and `start_fresh_cloud_launch()` (line 12938) handle `WorkspaceAction::OpenLocalToCloudHandoffPane`. The workspace is also where top-level overlays like `remove_tab_config_confirmation_dialog` are owned and rendered.
## Proposed changes
### 1. New view: `HandoffEnvironmentCreationModal`
New file: `app/src/settings_view/handoff_environment_creation_modal.rs`
A thin modal wrapper around `UpdateEnvironmentForm`, following the `AgentAssistedEnvironmentModal` pattern for rendering and `FirstTimeCloudAgentSetupView` for form configuration and environment creation logic.
**View state:**
- `visible: bool`
- `environment_form: ViewHandle<UpdateEnvironmentForm>`
- `close_button_mouse_state: MouseStateHandle`
- `scroll_state: ClippedScrollStateHandle` (the form is tall — needs scrolling within the modal)
**Public API:**
- `show(&mut self, ctx)` — sets `visible = true`, resets form to `Create` mode, focuses name field
- `hide(&mut self, ctx)` — sets `visible = false`
**Events:**
```rust
enum HandoffEnvironmentCreationModalEvent {
Created { env_id: SyncId },
Cancelled,
CreationFailed { error_message: String },
}
```
The `Created` event carries a `SyncId::ServerId` — guaranteed to be a server-recognized ID. The modal uses `create_ambient_agent_environment_online` (an inline server call with built-in retries) so the `ServerId` is available before the event is emitted, eliminating any `ClientId` → `ServerId` sync race. On failure, `CreationFailed` is emitted with the error message so the workspace can show a toast.
**Form configuration** (matching `FirstTimeCloudAgentSetupView`):
- `show_header = false` → submit button renders at bottom-right of form body
- `should_handle_escape_from_editor = true` → Escape in any editor emits `Cancelled`
- `auth_source = AuthSource::CloudSetup` → GitHub auth redirects back in-place
**Environment creation** (inside the modal's `handle_environment_form_event`):
- On `UpdateEnvironmentFormEvent::Created { environment, share_with_team }`:
1. Resolve owner via `cloud_environments::owner_for_new_environment()` / `owner_for_new_personal_environment()`
2. Generate `ClientId::default()`
3. Call `UpdateManager::create_ambient_agent_environment_online()` — returns `Future<Result<ServerId>>`
4. Hide the modal immediately
5. `ctx.spawn` the future:
- On `Ok(server_id)`: emit `Created { env_id: SyncId::ServerId(server_id) }`
- On `Err(err)`: log the error and emit `CreationFailed { error_message }`
**Rendering** (following `AgentAssistedEnvironmentModal`):
- When `visible == false`, render `Empty`
- When visible: `Dialog::new("Create environment", None, dialog_styles(appearance)).with_close_button(...).with_child(scrollable_form).with_width(DIALOG_WIDTH).build()` → `Dismiss::new().prevent_interaction_with_other_elements().on_dismiss(cancel)` → `Container` with dark overlay background + window corner radius
- The form content has no fixed max height — the dialog sizes to its content, with a `ClippedScrollable` safety net for very small windows
### 2. Input: intercept Enter when no environments exist
In `input.rs`, `maybe_launch_cloud_handoff_request()`:
After the existing empty-prompt early return (which already handles the no-op case), check whether environments are empty. If so, emit `Event::OpenHandoffEnvironmentCreationModal` and return early instead of collecting attachments. A new `OpenHandoffEnvironmentCreationModal` variant is added to `Input::Event`.
The prompt and attachments stay in the input buffer — the user will see them unchanged when the modal closes.
### 3. Workspace: own the modal and wire up handoff auto-submit
In `workspace/view.rs`:
**New field** on `Workspace`:
```rust
handoff_environment_creation_modal: Option<ViewHandle<HandoffEnvironmentCreationModal>>,
```
The modal is created on-demand (not pre-constructed at workspace init) to avoid unnecessary overhead. It is stored as `Option<ViewHandle>` and rendered as a `ChildView` in the workspace's `render()` method when `Some`, following the same pattern as `lightbox_view`.
**Subscribe to modal events** when the modal is created in `show_handoff_environment_creation_modal`:
- `HandoffEnvironmentCreationModalEvent::Created { env_id }` →
1. Set `handoff_environment_creation_modal = None`
2. Get the active terminal view's input and read the prompt + attachments from its `&` compose state
3. Use the input's `collect_cloud_launch_attachments()` and `editor.buffer_text()` to build a `PendingCloudLaunch`
4. Clear the input buffer and exit `&` compose mode
5. Dispatch `WorkspaceAction::OpenLocalToCloudHandoffPane { launch, explicit_environment_id: Some(env_id) }`
- `HandoffEnvironmentCreationModalEvent::Cancelled` →
1. Set `handoff_environment_creation_modal = None`
2. Re-focus the active terminal input (the `&` compose state and prompt are already preserved)
- `HandoffEnvironmentCreationModalEvent::CreationFailed { error_message }` →
1. Set `handoff_environment_creation_modal = None`
2. Show an error toast: "Failed to create environment: \<error_message\>"
3. Re-focus the active terminal input
**New workspace action** `ShowHandoffEnvironmentCreationModal` — creates the modal on-demand, subscribes to its events, stores the handle, and calls `ctx.notify()` to trigger a re-render. The terminal view subscribes to `Input::Event::OpenHandoffEnvironmentCreationModal` and dispatches this action.
**Render** — add the modal overlay to the workspace's render output when `handoff_environment_creation_modal.is_some()`, using the same overlay stacking pattern as `lightbox_view`.
### 4. Ghost text fallback
In `input.rs`, `set_zero_state_hint_text()` (line ~6120), the handoff compose branch falls back to `CLOUD_HANDOFF_HINT_TEXT` when no environment is found. Update this constant from `"Start a cloud run"` to `"Handoff to cloud"` to match behavior 2 in `PRODUCT.md`.
## Testing and validation
**Unit tests** (in `handoff_compose_tests.rs` and `input_test.rs`):
- Test that `maybe_launch_cloud_handoff_request` emits `OpenHandoffEnvironmentCreationModal` when environments list is empty and prompt is non-empty (behavior 5)
- Test that empty prompt + no environments returns `true` without emitting (behavior 6)
- Test that with environments present, Enter submits normally (behavior 16)
**Unit tests** (new file `handoff_environment_creation_modal_tests.rs`):
- Test `show()` resets form to Create mode and sets `visible = true`
- Test that form `Created` event triggers `UpdateManager::create_ambient_agent_environment` and emits `Created { env_id }`
- Test that form `Cancelled` event emits `Cancelled`
**Manual validation:**
- Enter `&` with no environments → ghost text shows "Handoff to cloud"
- Type a prompt and press Enter → modal opens with environment creation form, prompt preserved in input behind modal
- Fill out form, submit → environment created, handoff auto-submits with new environment
- Escape from modal → returns to `&` compose with prompt intact
- Create env via Settings while in `&` mode → chip updates reactively (behavior 17)