Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
const [sshCommand, copySSHCommand] = getSSHCommand(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 hasIDE = !!configuration.ide;
const openInIDEUrl = hasIDE
? `${configuration.ide}://vscode-remote/ssh-remote+${run.run_spec.run_name}${workingDir}`
: undefined;
// The IDE deep link is built server-side, per IDE, in JobConnectionInfo.attached_ide_url
// (e.g. `zed://ssh/...` for Zed vs `...//vscode-remote/ssh-remote+...` for VS Code forks).
// It is set once the job is running and reachable via the SSH config alias created by
// `dstack attach`. The UI always talks to a same-version server, so no fallback is needed.
const openInIDEUrl = run.jobs[0]?.job_connection_info?.attached_ide_url ?? undefined;
const ideDisplayName = hasIDE ? getIDEDisplayName(configuration.ide!) : undefined;

const [configCliCommand, copyCliCommand] = useConfigProjectCliCommand({ projectName: run.project_name });
Expand Down Expand Up @@ -222,6 +222,7 @@ export const ConnectToRunWithDevEnvConfiguration: FC<{ run: IRun }> = ({ run })
<Button
variant="primary"
external={true}
disabled={!openInIDEUrl}
onClick={() => window.open(openInIDEUrl, '_blank')}
>
Open in {ideDisplayName}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/pages/Runs/Launch/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ export const IDE_OPTIONS = [
label: 'Windsurf',
value: 'windsurf',
},
{
label: 'Zed',
value: 'zed',
},
] as const;

export const IDE_DISPLAY_NAMES: Record<string, string> = {
cursor: 'Cursor',
vscode: 'VS Code',
windsurf: 'Windsurf',
zed: 'Zed',
};

export const getIDEDisplayName = (ide: string): string => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Runs/Launch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface IRunEnvironmentFormValues {
gpu_enabled?: boolean;
offer?: IGpu;
name: string;
ide?: 'cursor' | 'vscode' | 'windsurf';
ide?: 'cursor' | 'vscode' | 'windsurf' | 'zed';
config_yaml: string;
image?: string;
python?: string;
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/types/run.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ declare type TGPUResources = IGPUSpecRequest & {
name?: string | string[];
};

declare type TIde = 'cursor' | 'vscode' | 'windsurf';
declare type TIde = 'cursor' | 'vscode' | 'windsurf' | 'zed';

declare type TVolumeMountPointRequest = {
name: string | string[];
Expand Down Expand Up @@ -315,9 +315,21 @@ declare interface IJobSubmission {
probes?: Array<{ success_streak: number }>;
}

declare interface IJobConnectionInfo {
ide_name?: string | null;
attached_ide_url?: string | null;
proxied_ide_url?: string | null;
attached_ssh_command?: string[] | null;
proxied_ssh_command?: string[] | null;
sshproxy_hostname?: string | null;
sshproxy_port?: number | null;
sshproxy_upstream_id?: string | null;
}

declare interface IJob {
job_spec: IJobSpec;
job_submissions: IJobSubmission[];
job_connection_info?: IJobConnectionInfo | null;
}

declare interface ISchedule {
Expand Down
46 changes: 42 additions & 4 deletions src/tests/_internal/server/routers/test_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,39 @@ async def test_patches_service_configuration_probes_for_old_clients(
assert response.json()["run_spec"]["configuration"]["probes"] == expected_probes

@pytest.mark.asyncio
@pytest.mark.parametrize(
("ide", "ide_name", "attached_ide_url", "proxied_ide_url_tmpl"),
[
pytest.param(
"vscode",
"VS Code",
"vscode://vscode-remote/ssh-remote+dev-env/test",
"vscode://vscode-remote/ssh-remote+{auth}/test",
id="vscode",
),
pytest.param(
"cursor",
"Cursor",
"cursor://vscode-remote/ssh-remote+dev-env/test",
"cursor://vscode-remote/ssh-remote+{auth}/test",
id="cursor",
),
pytest.param(
"windsurf",
"Windsurf",
"windsurf://vscode-remote/ssh-remote+dev-env/test",
"windsurf://vscode-remote/ssh-remote+{auth}/test",
id="windsurf",
),
pytest.param(
"zed",
"Zed",
"zed://ssh/dev-env/test",
"zed://ssh/{auth}/test",
id="zed",
),
],
)
@pytest.mark.parametrize(
"sshproxy",
[
Expand All @@ -1158,6 +1191,10 @@ async def test_returns_run_with_job_connection_info_dev_environment(
session: AsyncSession,
client: AsyncClient,
sshproxy: bool,
ide: str,
ide_name: str,
attached_ide_url: str,
proxied_ide_url_tmpl: str,
):
monkeypatch.setattr("dstack._internal.server.settings.SSHPROXY_ENABLED", sshproxy)
monkeypatch.setattr("dstack._internal.server.settings.SSHPROXY_HOSTNAME", "example.com")
Expand All @@ -1174,7 +1211,7 @@ async def test_returns_run_with_job_connection_info_dev_environment(
run_spec = get_run_spec(
repo_id=repo.name,
run_name="dev-env",
configuration=DevEnvironmentConfiguration(ide="cursor"),
configuration=DevEnvironmentConfiguration(ide=ide),
)
run = await create_run(
session=session,
Expand All @@ -1194,10 +1231,11 @@ async def test_returns_run_with_job_connection_info_dev_environment(
json={"run_name": run.run_name},
)
assert response.status_code == 200, response.json()
proxied_authority = f"{job.id.hex}@example.com:2222"
assert response.json()["jobs"][0]["job_connection_info"] == {
"ide_name": "Cursor",
"attached_ide_url": "cursor://vscode-remote/ssh-remote+dev-env/test",
"proxied_ide_url": f"cursor://vscode-remote/ssh-remote+{job.id.hex}@example.com:2222/test"
"ide_name": ide_name,
"attached_ide_url": attached_ide_url,
"proxied_ide_url": proxied_ide_url_tmpl.format(auth=proxied_authority)
if sshproxy
else None,
"attached_ssh_command": ["ssh", "dev-env"],
Expand Down
Loading