From 78119510d8eafc3f1feba7f52dec8e4a59cb67fc Mon Sep 17 00:00:00 2001 From: Andrey Cheptsov Date: Thu, 12 Mar 2026 12:13:12 +0100 Subject: [PATCH] Make `ide` optional for dev-environment (#1605) Allow dev environments to be provisioned without a pre-installed IDE, accessible via SSH only. When `ide` is omitted, no IDE setup or readme commands are run. Update frontend to support the None option and adjust validation, docs, and tests accordingly. Co-Authored-By: Claude Opus 4.6 --- docs/docs/concepts/dev-environments.md | 24 +++- frontend/src/locale/en.json | 2 +- .../index.tsx | 125 +++++++++++------- .../components/ParamsWizardStep/index.tsx | 7 +- frontend/src/pages/Runs/Launch/constants.tsx | 4 + .../Launch/hooks/useValidationResolver.ts | 2 +- frontend/src/pages/Runs/Launch/types.ts | 2 +- frontend/src/types/run.d.ts | 2 +- .../_internal/core/models/configurations.py | 10 +- .../server/services/jobs/configurators/dev.py | 34 +++-- .../core/models/test_configurations.py | 9 ++ 11 files changed, 151 insertions(+), 70 deletions(-) diff --git a/docs/docs/concepts/dev-environments.md b/docs/docs/concepts/dev-environments.md index d946e539b..c4bb01565 100644 --- a/docs/docs/concepts/dev-environments.md +++ b/docs/docs/concepts/dev-environments.md @@ -5,7 +5,7 @@ description: Provisioning remote instances for cloud-based development # Dev environments -A dev environment lets you provision an instance and access it with your desktop IDE. +A dev environment lets you provision an instance and access it with your desktop IDE or SSH. ??? info "Prerequisites" Before running a dev environment, make sure you’ve [installed](../installation.md) the server and CLI, and created a [fleet](fleets.md). @@ -25,6 +25,8 @@ name: vscode python: "3.11" # Uncomment to use a custom Docker image #image: huggingface/trl-latest-gpu + +# Comment if not required ide: vscode # Uncomment to leverage spot instances @@ -55,12 +57,32 @@ Launching `vscode`... To open in VS Code Desktop, use this link: vscode://vscode-remote/ssh-remote+vscode/workflow + +To connect via SSH, use: `ssh vscode` ``` `dstack apply` automatically provisions an instance and sets up an IDE on it. +??? info "SSH-only" + The `ide` property is optional. If omitted, no IDE is pre-installed, but the dev environment + is still accessible via SSH: + +
+ + ```yaml + type: dev-environment + name: my-env + + python: "3.11" + + resources: + gpu: 24GB + ``` + +
+ ??? info "Windows" On Windows, `dstack` works both natively and inside WSL. But, for dev environments, it's recommended _not to use_ `dstack apply` _inside WSL_ due to a [VS Code issue](https://github.com/microsoft/vscode-remote-release/issues/937). diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 2436f64f5..ae5144acc 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -517,7 +517,7 @@ "name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.", "name_placeholder": "Optional", "ide": "IDE", - "ide_description": "Select which IDE would you like to use with the dev environment.", + "ide_description": "Optionally select an IDE to pre-install in the dev environment.", "docker": "Docker", "docker_image": "Image", "docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'", diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx index ad2c13fb5..ec5444c66 100644 --- a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx +++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx @@ -44,8 +44,11 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) const configuration = run.run_spec.configuration as TDevEnvironmentConfiguration; const latestSubmission = run.jobs[0]?.job_submissions?.slice(-1)[0]; const workingDir = latestSubmission?.job_runtime_data?.working_dir ?? '/'; - const openInIDEUrl = `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`; - const ideDisplayName = getIDEDisplayName(configuration.ide); + const hasIDE = !!configuration.ide; + const openInIDEUrl = hasIDE + ? `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}` + : undefined; + const ideDisplayName = hasIDE ? getIDEDisplayName(configuration.ide!) : undefined; const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name }); @@ -210,52 +213,82 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) ), isOptional: true, }, - { - title: 'Open', - description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`, - content: ( - - + hasIDE + ? { + title: 'Open', + description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`, + content: ( + + - - - -
- {sshCommand} + + + +
+ {sshCommand} -
- - {t('common.copied')} - - } - > -
-
-
-
- - ), - isOptional: true, - }, +
+ + {t('common.copied')} + + } + > +
+
+
+
+
+ ), + isOptional: true, + } + : { + title: 'Connect via SSH', + description: 'After the CLI is attached, you can connect to the dev environment via SSH.', + content: ( +
+ {sshCommand} + +
+ {t('common.copied')} + } + > +
+
+ ), + isOptional: true, + }, ]} /> )} diff --git a/frontend/src/pages/Runs/Launch/components/ParamsWizardStep/index.tsx b/frontend/src/pages/Runs/Launch/components/ParamsWizardStep/index.tsx index 5385e9989..9795b760e 100644 --- a/frontend/src/pages/Runs/Launch/components/ParamsWizardStep/index.tsx +++ b/frontend/src/pages/Runs/Launch/components/ParamsWizardStep/index.tsx @@ -94,6 +94,11 @@ export const ParamsWizardStep: React.FC = ({ formMethods, return null; } + const templateIde = + template?.configuration && 'ide' in template.configuration + ? ((template.configuration as TDevEnvironmentConfiguration).ide ?? '') + : ''; + return ( = ({ formMethods, name={FORM_FIELD_NAMES.ide} options={IDE_OPTIONS} disabled={loading} - defaultValue={'cursor'} + defaultValue={templateIde} /> ); }; diff --git a/frontend/src/pages/Runs/Launch/constants.tsx b/frontend/src/pages/Runs/Launch/constants.tsx index 7afd83a70..f9b25bc26 100644 --- a/frontend/src/pages/Runs/Launch/constants.tsx +++ b/frontend/src/pages/Runs/Launch/constants.tsx @@ -64,6 +64,10 @@ export const FORM_FIELD_NAMES = { } as const satisfies Record; export const IDE_OPTIONS = [ + { + label: 'None', + value: '', + }, { label: 'Cursor', value: 'cursor', diff --git a/frontend/src/pages/Runs/Launch/hooks/useValidationResolver.ts b/frontend/src/pages/Runs/Launch/hooks/useValidationResolver.ts index 41ef5d04b..45420773e 100644 --- a/frontend/src/pages/Runs/Launch/hooks/useValidationResolver.ts +++ b/frontend/src/pages/Runs/Launch/hooks/useValidationResolver.ts @@ -25,7 +25,7 @@ export const useYupValidationResolver = (template?: ITemplate) => { break; case 'ide': - schema['ide'] = yup.string().required(requiredFieldError); + schema['ide'] = yup.string().nullable(); break; case 'resources': diff --git a/frontend/src/pages/Runs/Launch/types.ts b/frontend/src/pages/Runs/Launch/types.ts index ef3c164f0..20a5240d5 100644 --- a/frontend/src/pages/Runs/Launch/types.ts +++ b/frontend/src/pages/Runs/Launch/types.ts @@ -4,7 +4,7 @@ export interface IRunEnvironmentFormValues { gpu_enabled?: boolean; offer?: IGpu; name: string; - ide: 'cursor' | 'vscode' | 'windsurf'; + ide?: 'cursor' | 'vscode' | 'windsurf'; config_yaml: string; image?: string; python?: string; diff --git a/frontend/src/types/run.d.ts b/frontend/src/types/run.d.ts index b72312daf..3dcb26446 100644 --- a/frontend/src/types/run.d.ts +++ b/frontend/src/types/run.d.ts @@ -159,7 +159,7 @@ declare type TServiceConfiguration = TBaseConfiguration & { declare type TDevEnvironmentConfiguration = TBaseConfiguration & { type?: 'dev-environment'; - ide: TIde; + ide?: TIde | null; version?: string; init?: string[]; inactivity_duration?: string | number | boolean | 'off'; diff --git a/src/dstack/_internal/core/models/configurations.py b/src/dstack/_internal/core/models/configurations.py index ac8d8d172..cb383aa82 100644 --- a/src/dstack/_internal/core/models/configurations.py +++ b/src/dstack/_internal/core/models/configurations.py @@ -645,11 +645,11 @@ def check_image_or_commands_present(cls, values): class DevEnvironmentConfigurationParams(CoreModel): ide: Annotated[ - Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]], + Optional[Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]]], Field( - description="The IDE to run. Supported values include `vscode`, `cursor`, and `windsurf`" + description="The IDE to pre-install. Supported values include `vscode`, `cursor`, and `windsurf`. Defaults to no IDE (SSH only)" ), - ] + ] = None version: Annotated[ Optional[str], Field( @@ -683,9 +683,11 @@ def parse_inactivity_duration( return None @root_validator - def validate_windsurf_version_format(cls, values): + def validate_ide_and_version(cls, values): ide = values.get("ide") version = values.get("version") + if version and ide is None: + raise ValueError("`version` requires `ide` to be set") if ide == "windsurf" and version: # Validate format: version@commit if not re.match(r"^.+@[a-f0-9]+$", version): diff --git a/src/dstack/_internal/server/services/jobs/configurators/dev.py b/src/dstack/_internal/server/services/jobs/configurators/dev.py index 42bdc4376..ae059b9e0 100644 --- a/src/dstack/_internal/server/services/jobs/configurators/dev.py +++ b/src/dstack/_internal/server/services/jobs/configurators/dev.py @@ -23,30 +23,36 @@ def __init__( ): assert run_spec.configuration.type == "dev-environment" - if run_spec.configuration.ide == "vscode": - __class = VSCodeDesktop - elif run_spec.configuration.ide == "cursor": - __class = CursorDesktop - elif run_spec.configuration.ide == "windsurf": - __class = WindsurfDesktop + if run_spec.configuration.ide is None: + self.ide = None else: - raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}") - self.ide = __class( - run_name=run_spec.run_name, - version=run_spec.configuration.version, - extensions=["ms-python.python", "ms-toolsai.jupyter"], - ) + if run_spec.configuration.ide == "vscode": + __class = VSCodeDesktop + elif run_spec.configuration.ide == "cursor": + __class = CursorDesktop + elif run_spec.configuration.ide == "windsurf": + __class = WindsurfDesktop + else: + raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}") + self.ide = __class( + run_name=run_spec.run_name, + version=run_spec.configuration.version, + extensions=["ms-python.python", "ms-toolsai.jupyter"], + ) super().__init__(run_spec=run_spec, secrets=secrets, replica_group_name=replica_group_name) def _shell_commands(self) -> List[str]: assert self.run_spec.configuration.type == "dev-environment" - commands = self.ide.get_install_commands() + commands = [] + if self.ide is not None: + commands += self.ide.get_install_commands() commands.append(INSTALL_IPYKERNEL) commands += self.run_spec.configuration.setup commands.append("echo") commands += self.run_spec.configuration.init - commands += self.ide.get_print_readme_commands() + if self.ide is not None: + commands += self.ide.get_print_readme_commands() commands += [ f"echo 'To connect via SSH, use: `ssh {self.run_spec.run_name}`'", "echo", diff --git a/src/tests/_internal/core/models/test_configurations.py b/src/tests/_internal/core/models/test_configurations.py index 44c31f5cb..884e72ebc 100644 --- a/src/tests/_internal/core/models/test_configurations.py +++ b/src/tests/_internal/core/models/test_configurations.py @@ -230,3 +230,12 @@ def test_cursor_version_not_validated(self): params = DevEnvironmentConfigurationParams(ide="cursor", version="0.40.0") assert params.ide == "cursor" assert params.version == "0.40.0" + + def test_ide_optional(self): + params = DevEnvironmentConfigurationParams() + assert params.ide is None + assert params.version is None + + def test_version_requires_ide(self): + with pytest.raises(ValueError, match="`version` requires `ide` to be set"): + DevEnvironmentConfigurationParams(version="1.80.0")