138 lines
9.3 KiB
Markdown
138 lines
9.3 KiB
Markdown
# 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)
|