Skip to content

Commit 7811951

Browse files
Andrey Cheptsovclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 130b22b commit 7811951

11 files changed

Lines changed: 151 additions & 70 deletions

File tree

docs/docs/concepts/dev-environments.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Provisioning remote instances for cloud-based development
55

66
# Dev environments
77

8-
A dev environment lets you provision an instance and access it with your desktop IDE.
8+
A dev environment lets you provision an instance and access it with your desktop IDE or SSH.
99

1010
??? info "Prerequisites"
1111
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
2525
python: "3.11"
2626
# Uncomment to use a custom Docker image
2727
#image: huggingface/trl-latest-gpu
28+
29+
# Comment if not required
2830
ide: vscode
2931

3032
# Uncomment to leverage spot instances
@@ -55,12 +57,32 @@ Launching `vscode`...
5557

5658
To open in VS Code Desktop, use this link:
5759
vscode://vscode-remote/ssh-remote+vscode/workflow
60+
61+
To connect via SSH, use: `ssh vscode`
5862
```
5963
6064
</div>
6165
6266
`dstack apply` automatically provisions an instance and sets up an IDE on it.
6367

68+
??? info "SSH-only"
69+
The `ide` property is optional. If omitted, no IDE is pre-installed, but the dev environment
70+
is still accessible via SSH:
71+
72+
<div editor-title=".dstack.yml">
73+
74+
```yaml
75+
type: dev-environment
76+
name: my-env
77+
78+
python: "3.11"
79+
80+
resources:
81+
gpu: 24GB
82+
```
83+
84+
</div>
85+
6486
??? info "Windows"
6587
On Windows, `dstack` works both natively and inside WSL. But, for dev environments,
6688
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).

frontend/src/locale/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@
517517
"name_constraint": "Example: 'my-fleet' or 'default'. If not specified, generated automatically.",
518518
"name_placeholder": "Optional",
519519
"ide": "IDE",
520-
"ide_description": "Select which IDE would you like to use with the dev environment.",
520+
"ide_description": "Optionally select an IDE to pre-install in the dev environment.",
521521
"docker": "Docker",
522522
"docker_image": "Image",
523523
"docker_image_description": "A Docker image name, e.g. 'lmsysorg/sglang:latest'",

frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
4444
const configuration = run.run_spec.configuration as TDevEnvironmentConfiguration;
4545
const latestSubmission = run.jobs[0]?.job_submissions?.slice(-1)[0];
4646
const workingDir = latestSubmission?.job_runtime_data?.working_dir ?? '/';
47-
const openInIDEUrl = `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`;
48-
const ideDisplayName = getIDEDisplayName(configuration.ide);
47+
const hasIDE = !!configuration.ide;
48+
const openInIDEUrl = hasIDE
49+
? `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`
50+
: undefined;
51+
const ideDisplayName = hasIDE ? getIDEDisplayName(configuration.ide!) : undefined;
4952

5053
const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name });
5154

@@ -210,52 +213,82 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
210213
),
211214
isOptional: true,
212215
},
213-
{
214-
title: 'Open',
215-
description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`,
216-
content: (
217-
<SpaceBetween size="s">
218-
<Button
219-
variant="primary"
220-
external={true}
221-
onClick={() => window.open(openInIDEUrl, '_blank')}
222-
>
223-
Open in {ideDisplayName}
224-
</Button>
216+
hasIDE
217+
? {
218+
title: 'Open',
219+
description: `After the CLI is attached, you can open the dev environment in ${ideDisplayName}.`,
220+
content: (
221+
<SpaceBetween size="s">
222+
<Button
223+
variant="primary"
224+
external={true}
225+
onClick={() => window.open(openInIDEUrl, '_blank')}
226+
>
227+
Open in {ideDisplayName}
228+
</Button>
225229

226-
<ExpandableSection headerText="Need plain SSH?">
227-
<SpaceBetween size="s">
228-
<Box />
229-
<div className={styles.codeWrapper}>
230-
<Code className={styles.code}>{sshCommand}</Code>
230+
<ExpandableSection headerText="Need plain SSH?">
231+
<SpaceBetween size="s">
232+
<Box />
233+
<div className={styles.codeWrapper}>
234+
<Code className={styles.code}>{sshCommand}</Code>
231235

232-
<div className={styles.copy}>
233-
<Popover
234-
dismissButton={false}
235-
position="top"
236-
size="small"
237-
triggerType="custom"
238-
content={
239-
<StatusIndicator type="success">
240-
{t('common.copied')}
241-
</StatusIndicator>
242-
}
243-
>
244-
<Button
245-
formAction="none"
246-
iconName="copy"
247-
variant="normal"
248-
onClick={() => copySSHCommand()}
249-
/>
250-
</Popover>
251-
</div>
252-
</div>
253-
</SpaceBetween>
254-
</ExpandableSection>
255-
</SpaceBetween>
256-
),
257-
isOptional: true,
258-
},
236+
<div className={styles.copy}>
237+
<Popover
238+
dismissButton={false}
239+
position="top"
240+
size="small"
241+
triggerType="custom"
242+
content={
243+
<StatusIndicator type="success">
244+
{t('common.copied')}
245+
</StatusIndicator>
246+
}
247+
>
248+
<Button
249+
formAction="none"
250+
iconName="copy"
251+
variant="normal"
252+
onClick={() => copySSHCommand()}
253+
/>
254+
</Popover>
255+
</div>
256+
</div>
257+
</SpaceBetween>
258+
</ExpandableSection>
259+
</SpaceBetween>
260+
),
261+
isOptional: true,
262+
}
263+
: {
264+
title: 'Connect via SSH',
265+
description: 'After the CLI is attached, you can connect to the dev environment via SSH.',
266+
content: (
267+
<div className={styles.codeWrapper}>
268+
<Code className={styles.code}>{sshCommand}</Code>
269+
270+
<div className={styles.copy}>
271+
<Popover
272+
dismissButton={false}
273+
position="top"
274+
size="small"
275+
triggerType="custom"
276+
content={
277+
<StatusIndicator type="success">{t('common.copied')}</StatusIndicator>
278+
}
279+
>
280+
<Button
281+
formAction="none"
282+
iconName="copy"
283+
variant="normal"
284+
onClick={() => copySSHCommand()}
285+
/>
286+
</Popover>
287+
</div>
288+
</div>
289+
),
290+
isOptional: true,
291+
},
259292
]}
260293
/>
261294
)}

frontend/src/pages/Runs/Launch/components/ParamsWizardStep/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ export const ParamsWizardStep: React.FC<ParamsWizardStepProps> = ({ formMethods,
9494
return null;
9595
}
9696

97+
const templateIde =
98+
template?.configuration && 'ide' in template.configuration
99+
? ((template.configuration as TDevEnvironmentConfiguration).ide ?? '')
100+
: '';
101+
97102
return (
98103
<FormSelect
99104
label={t('runs.launch.wizard.ide')}
@@ -102,7 +107,7 @@ export const ParamsWizardStep: React.FC<ParamsWizardStepProps> = ({ formMethods,
102107
name={FORM_FIELD_NAMES.ide}
103108
options={IDE_OPTIONS}
104109
disabled={loading}
105-
defaultValue={'cursor'}
110+
defaultValue={templateIde}
106111
/>
107112
);
108113
};

frontend/src/pages/Runs/Launch/constants.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export const FORM_FIELD_NAMES = {
6464
} as const satisfies Record<IRunEnvironmentFormKeys, IRunEnvironmentFormKeys>;
6565

6666
export const IDE_OPTIONS = [
67+
{
68+
label: 'None',
69+
value: '',
70+
},
6771
{
6872
label: 'Cursor',
6973
value: 'cursor',

frontend/src/pages/Runs/Launch/hooks/useValidationResolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const useYupValidationResolver = (template?: ITemplate) => {
2525
break;
2626

2727
case 'ide':
28-
schema['ide'] = yup.string().required(requiredFieldError);
28+
schema['ide'] = yup.string().nullable();
2929
break;
3030

3131
case 'resources':

frontend/src/pages/Runs/Launch/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface IRunEnvironmentFormValues {
44
gpu_enabled?: boolean;
55
offer?: IGpu;
66
name: string;
7-
ide: 'cursor' | 'vscode' | 'windsurf';
7+
ide?: 'cursor' | 'vscode' | 'windsurf';
88
config_yaml: string;
99
image?: string;
1010
python?: string;

frontend/src/types/run.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ declare type TServiceConfiguration = TBaseConfiguration & {
159159

160160
declare type TDevEnvironmentConfiguration = TBaseConfiguration & {
161161
type?: 'dev-environment';
162-
ide: TIde;
162+
ide?: TIde | null;
163163
version?: string;
164164
init?: string[];
165165
inactivity_duration?: string | number | boolean | 'off';

src/dstack/_internal/core/models/configurations.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -645,11 +645,11 @@ def check_image_or_commands_present(cls, values):
645645

646646
class DevEnvironmentConfigurationParams(CoreModel):
647647
ide: Annotated[
648-
Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]],
648+
Optional[Union[Literal["vscode"], Literal["cursor"], Literal["windsurf"]]],
649649
Field(
650-
description="The IDE to run. Supported values include `vscode`, `cursor`, and `windsurf`"
650+
description="The IDE to pre-install. Supported values include `vscode`, `cursor`, and `windsurf`. Defaults to no IDE (SSH only)"
651651
),
652-
]
652+
] = None
653653
version: Annotated[
654654
Optional[str],
655655
Field(
@@ -683,9 +683,11 @@ def parse_inactivity_duration(
683683
return None
684684

685685
@root_validator
686-
def validate_windsurf_version_format(cls, values):
686+
def validate_ide_and_version(cls, values):
687687
ide = values.get("ide")
688688
version = values.get("version")
689+
if version and ide is None:
690+
raise ValueError("`version` requires `ide` to be set")
689691
if ide == "windsurf" and version:
690692
# Validate format: version@commit
691693
if not re.match(r"^.+@[a-f0-9]+$", version):

src/dstack/_internal/server/services/jobs/configurators/dev.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,36 @@ def __init__(
2323
):
2424
assert run_spec.configuration.type == "dev-environment"
2525

26-
if run_spec.configuration.ide == "vscode":
27-
__class = VSCodeDesktop
28-
elif run_spec.configuration.ide == "cursor":
29-
__class = CursorDesktop
30-
elif run_spec.configuration.ide == "windsurf":
31-
__class = WindsurfDesktop
26+
if run_spec.configuration.ide is None:
27+
self.ide = None
3228
else:
33-
raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}")
34-
self.ide = __class(
35-
run_name=run_spec.run_name,
36-
version=run_spec.configuration.version,
37-
extensions=["ms-python.python", "ms-toolsai.jupyter"],
38-
)
29+
if run_spec.configuration.ide == "vscode":
30+
__class = VSCodeDesktop
31+
elif run_spec.configuration.ide == "cursor":
32+
__class = CursorDesktop
33+
elif run_spec.configuration.ide == "windsurf":
34+
__class = WindsurfDesktop
35+
else:
36+
raise ServerClientError(f"Unsupported IDE: {run_spec.configuration.ide}")
37+
self.ide = __class(
38+
run_name=run_spec.run_name,
39+
version=run_spec.configuration.version,
40+
extensions=["ms-python.python", "ms-toolsai.jupyter"],
41+
)
3942
super().__init__(run_spec=run_spec, secrets=secrets, replica_group_name=replica_group_name)
4043

4144
def _shell_commands(self) -> List[str]:
4245
assert self.run_spec.configuration.type == "dev-environment"
4346

44-
commands = self.ide.get_install_commands()
47+
commands = []
48+
if self.ide is not None:
49+
commands += self.ide.get_install_commands()
4550
commands.append(INSTALL_IPYKERNEL)
4651
commands += self.run_spec.configuration.setup
4752
commands.append("echo")
4853
commands += self.run_spec.configuration.init
49-
commands += self.ide.get_print_readme_commands()
54+
if self.ide is not None:
55+
commands += self.ide.get_print_readme_commands()
5056
commands += [
5157
f"echo 'To connect via SSH, use: `ssh {self.run_spec.run_name}`'",
5258
"echo",

0 commit comments

Comments
 (0)