diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx
new file mode 100644
index 000000000..36bfa2379
--- /dev/null
+++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/index.tsx
@@ -0,0 +1,244 @@
+import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import {
+ Alert,
+ Box,
+ Button,
+ Code,
+ Container,
+ ExpandableSection,
+ Header,
+ Popover,
+ SpaceBetween,
+ StatusIndicator,
+ Tabs,
+ Wizard,
+} from 'components';
+
+import { copyToClipboard } from 'libs';
+
+import styles from './styles.module.scss';
+
+const UvInstallCommand = 'uv tool install dstack -U';
+const PipInstallCommand = 'pip install dstack -U';
+
+export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run }) => {
+ const { t } = useTranslation();
+
+ const getAttachCommand = (runData: IRun) => {
+ const attachCommand = `dstack attach ${runData.run_spec.run_name}`;
+
+ const copyAttachCommand = () => {
+ copyToClipboard(attachCommand);
+ };
+
+ return [attachCommand, copyAttachCommand] as const;
+ };
+
+ const getSSHCommand = (runData: IRun) => {
+ const sshCommand = `ssh ${runData.run_spec.run_name}`;
+
+ const copySSHCommand = () => {
+ copyToClipboard(sshCommand);
+ };
+
+ return [sshCommand, copySSHCommand] as const;
+ };
+
+ const [activeStepIndex, setActiveStepIndex] = React.useState(0);
+ const [attachCommand, copyAttachCommand] = getAttachCommand(run);
+ const [sshCommand, copySSHCommand] = getSSHCommand(run);
+
+ const openInIDEUrl = `${run.run_spec.configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}/${run.run_spec.working_dir || 'workflow'}`;
+
+ return (
+
+
+
+ {run.status === 'running' && (
+ `Step ${stepNumber}`,
+ collapsedStepsLabel: (stepNumber, stepsCount) => `Step ${stepNumber} of ${stepsCount}`,
+ skipToButtonLabel: (step) => `Skip to ${step.title}`,
+ navigationAriaLabel: 'Steps',
+ previousButton: 'Previous',
+ nextButton: 'Next',
+ optional: 'required',
+ }}
+ onNavigate={({ detail }) => setActiveStepIndex(detail.requestedStepIndex)}
+ activeStepIndex={activeStepIndex}
+ onSubmit={() => window.open(openInIDEUrl, '_blank')}
+ submitButtonText="Open in VS Code"
+ allowSkipTo
+ steps={[
+ {
+ title: 'Attach',
+ content: (
+
+ To access this run, first you need to attach to it.
+
+
{attachCommand}
+
+
+
{t('common.copied')}}
+ >
+
+
+
+
+
+
+
+
+ To use dstack, install the CLI on your local machine.
+
+
+
+
{UvInstallCommand}
+
+
+
+ {t('common.copied')}
+
+ }
+ >
+
+
+
+ >
+ ),
+ },
+ {
+ label: 'pip',
+ id: 'pip',
+ content: (
+ <>
+
+
{PipInstallCommand}
+
+
+
+ {t('common.copied')}
+
+ }
+ >
+
+
+
+ >
+ ),
+ },
+ ]}
+ />
+
+
+
+ ),
+ isOptional: true,
+ },
+ {
+ title: 'Open',
+ description: 'After the CLI is attached, you can open the dev environment in VS Code.',
+ content: (
+
+
+
+
+
+
+
+
{sshCommand}
+
+
+
+ {t('common.copied')}
+
+ }
+ >
+
+
+
+
+
+
+ ),
+ isOptional: true,
+ },
+ ]}
+ />
+ )}
+
+ {run.status === 'running' && (
+
+
+ Waiting for the run to start.
+
+ )}
+
+ );
+};
diff --git a/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss
new file mode 100644
index 000000000..d01f54b26
--- /dev/null
+++ b/frontend/src/pages/Runs/Details/RunDetails/ConnectToRunWithDevEnvConfiguration/styles.module.scss
@@ -0,0 +1,13 @@
+.codeWrapper {
+ position: relative;
+
+ .code {
+ padding: 16px 12px;
+ }
+
+ .copy {
+ position: absolute;
+ top: 10px;
+ right: 8px;
+ }
+}
diff --git a/frontend/src/pages/Runs/Details/RunDetails/index.tsx b/frontend/src/pages/Runs/Details/RunDetails/index.tsx
index f5613a288..aab63a3ed 100644
--- a/frontend/src/pages/Runs/Details/RunDetails/index.tsx
+++ b/frontend/src/pages/Runs/Details/RunDetails/index.tsx
@@ -8,11 +8,12 @@ import { Box, ColumnLayout, Container, Header, Loader, NavigateLink, StatusIndic
import { DATE_TIME_FORMAT } from 'consts';
import { getRunError, getRunPriority, getRunStatusMessage, getStatusIconColor, getStatusIconType } from 'libs/run';
+import { ROUTES } from 'routes';
import { useGetRunQuery } from 'services/run';
import { finishedRunStatuses } from 'pages/Runs/constants';
+import { runIsStopped } from 'pages/Runs/utils';
-import { ROUTES } from '../../../../routes';
import {
getRunListItemBackend,
getRunListItemInstanceId,
@@ -23,6 +24,7 @@ import {
getRunListItemSpot,
} from '../../List/helpers';
import { JobList } from '../Jobs/List';
+import { ConnectToRunWithDevEnvConfiguration } from './ConnectToRunWithDevEnvConfiguration';
export const RunDetails = () => {
const { t } = useTranslation();
@@ -49,6 +51,7 @@ export const RunDetails = () => {
const status = finishedRunStatuses.includes(runData.status)
? (runData.latest_job_submission?.status ?? runData.status)
: runData.status;
+
const terminationReason = finishedRunStatuses.includes(runData.status)
? runData.latest_job_submission?.termination_reason
: null;
@@ -168,6 +171,10 @@ export const RunDetails = () => {
)}
+ {runData.run_spec.configuration.type === 'dev-environment' && !runIsStopped(runData.status) && (
+
+ )}
+
{runData.jobs.length > 1 && (