9.3 KiB
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 returnstrue(consumed) when the prompt is empty, or collects attachments and dispatchesWorkspaceAction::OpenLocalToCloudHandoffPane.app/src/terminal/input/handoff_compose.rs—HandoffComposeStatemodel 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—UpdateEnvironmentFormview. Already supports modal-style use viashow_header(line 332),should_handle_escape_from_editor(line 336), andauth_source(line 342). EmitsUpdateEnvironmentFormEvent::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—FirstTimeCloudAgentSetupViewwraps the form withshow_header=false,should_handle_escape_from_editor=true, handlesCreatedby callingUpdateManager::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—AgentAssistedEnvironmentModalrenders usingDialog::new().with_close_button().with_child().build()wrapped inDismiss::new().prevent_interaction_with_other_elements(), inside aContainerwithColorU::new(0, 0, 0, 179)background. Usesshow()/hide()visibility toggle and emitsCancelled/Confirmedevents. This is the rendering pattern to follow.app/src/ui_components/dialog.rs—Dialogcomponent 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) andstart_fresh_cloud_launch()(line 12938) handleWorkspaceAction::OpenLocalToCloudHandoffPane. The workspace is also where top-level overlays likeremove_tab_config_confirmation_dialogare 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: boolenvironment_form: ViewHandle<UpdateEnvironmentForm>close_button_mouse_state: MouseStateHandlescroll_state: ClippedScrollStateHandle(the form is tall — needs scrolling within the modal)
Public API:
show(&mut self, ctx)— setsvisible = true, resets form toCreatemode, focuses name fieldhide(&mut self, ctx)— setsvisible = false
Events:
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 bodyshould_handle_escape_from_editor = true→ Escape in any editor emitsCancelledauth_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 }:- Resolve owner via
cloud_environments::owner_for_new_environment()/owner_for_new_personal_environment() - Generate
ClientId::default() - Call
UpdateManager::create_ambient_agent_environment_online()— returnsFuture<Result<ServerId>> - Hide the modal immediately
ctx.spawnthe future:- On
Ok(server_id): emitCreated { env_id: SyncId::ServerId(server_id) } - On
Err(err): log the error and emitCreationFailed { error_message }
- On
- Resolve owner via
Rendering (following AgentAssistedEnvironmentModal):
- When
visible == false, renderEmpty - 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)→Containerwith dark overlay background + window corner radius - The form content has no fixed max height — the dialog sizes to its content, with a
ClippedScrollablesafety 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:
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 }→- Set
handoff_environment_creation_modal = None - Get the active terminal view's input and read the prompt + attachments from its
&compose state - Use the input's
collect_cloud_launch_attachments()andeditor.buffer_text()to build aPendingCloudLaunch - Clear the input buffer and exit
&compose mode - Dispatch
WorkspaceAction::OpenLocalToCloudHandoffPane { launch, explicit_environment_id: Some(env_id) }
- Set
HandoffEnvironmentCreationModalEvent::Cancelled→- Set
handoff_environment_creation_modal = None - Re-focus the active terminal input (the
&compose state and prompt are already preserved)
- Set
HandoffEnvironmentCreationModalEvent::CreationFailed { error_message }→- Set
handoff_environment_creation_modal = None - Show an error toast: "Failed to create environment: <error_message>"
- Re-focus the active terminal input
- Set
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_requestemitsOpenHandoffEnvironmentCreationModalwhen environments list is empty and prompt is non-empty (behavior 5) - Test that empty prompt + no environments returns
truewithout 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 setsvisible = true - Test that form
Createdevent triggersUpdateManager::create_ambient_agent_environmentand emitsCreated { env_id } - Test that form
Cancelledevent emitsCancelled
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)