diff --git a/README.md b/README.md
index 896fabd7..f94c0a2d 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ AI coding agents are increasingly writing the world's code and human engineers n
- Quickly review work and start dev servers
- Track the status of tasks that your coding agents are working on
- Centralise configuration of coding agent MCP configs
+- Open projects remotely via SSH when running Vibe Kanban on a remote server
You can watch a video overview [here](https://youtu.be/TFT3KnZOOAk).
@@ -119,3 +120,20 @@ By default, Vibe Kanban uses Bloop AI's GitHub OAuth app for authentication. To
```bash
GITHUB_CLIENT_ID=your_client_id_here pnpm run build
```
+
+### Remote Deployment
+
+When running Vibe Kanban on a remote server (e.g., via systemctl, Docker, or cloud hosting), you can configure your editor to open projects via SSH:
+
+1. **Access via tunnel**: Use Cloudflare Tunnel, ngrok, or similar to expose the web UI
+2. **Configure remote SSH** in Settings → Editor Integration:
+ - Set **Remote SSH Host** to your server hostname or IP
+ - Set **Remote SSH User** to your SSH username (optional)
+3. **Prerequisites**:
+ - SSH access from your local machine to the remote server
+ - SSH keys configured (passwordless authentication)
+ - VSCode Remote-SSH extension
+
+When configured, the "Open in VSCode" buttons will generate URLs like `vscode://vscode-remote/ssh-remote+user@host/path` that open your local editor and connect to the remote server.
+
+See the [documentation](https://vibekanban.com/docs/configuration-customisation/global-settings#remote-ssh-configuration) for detailed setup instructions.
diff --git a/crates/server/src/routes/projects.rs b/crates/server/src/routes/projects.rs
index 392d2c08..9cae92ae 100644
--- a/crates/server/src/routes/projects.rs
+++ b/crates/server/src/routes/projects.rs
@@ -264,12 +264,17 @@ pub struct OpenEditorRequest {
editor_type: Option,
}
+#[derive(Debug, serde::Serialize, ts_rs::TS)]
+pub struct OpenEditorResponse {
+ pub url: Option,
+}
+
pub async fn open_project_in_editor(
Extension(project): Extension,
State(deployment): State,
Json(payload): Json>,
-) -> Result>, StatusCode> {
- let path = project.git_repo_path.to_string_lossy();
+) -> Result>, StatusCode> {
+ let path = project.git_repo_path;
let editor_config = {
let config = deployment.config().read().await;
@@ -278,8 +283,13 @@ pub async fn open_project_in_editor(
};
match editor_config.open_file(&path) {
- Ok(_) => {
- tracing::info!("Opened editor for project {} at path: {}", project.id, path);
+ Ok(url) => {
+ tracing::info!(
+ "Opened editor for project {} at path: {}{}",
+ project.id,
+ path.to_string_lossy(),
+ if url.is_some() { " (remote mode)" } else { "" }
+ );
deployment
.track_if_analytics_allowed(
@@ -287,11 +297,14 @@ pub async fn open_project_in_editor(
serde_json::json!({
"project_id": project.id.to_string(),
"editor_type": payload.as_ref().and_then(|req| req.editor_type.as_ref()),
+ "remote_mode": url.is_some(),
}),
)
.await;
- Ok(ResponseJson(ApiResponse::success(())))
+ Ok(ResponseJson(ApiResponse::success(OpenEditorResponse {
+ url,
+ })))
}
Err(e) => {
tracing::error!("Failed to open editor for project {}: {}", project.id, e);
diff --git a/crates/server/src/routes/task_attempts.rs b/crates/server/src/routes/task_attempts.rs
index ee155325..29261ca7 100644
--- a/crates/server/src/routes/task_attempts.rs
+++ b/crates/server/src/routes/task_attempts.rs
@@ -823,11 +823,16 @@ pub struct OpenEditorRequest {
file_path: Option,
}
+#[derive(Debug, Serialize, TS)]
+pub struct OpenEditorResponse {
+ pub url: Option,
+}
+
pub async fn open_task_attempt_in_editor(
Extension(task_attempt): Extension,
State(deployment): State,
Json(payload): Json>,
-) -> Result>, ApiError> {
+) -> Result>, ApiError> {
// Get the task attempt to access the worktree path
let base_path_buf = ensure_worktree_path(&deployment, &task_attempt).await?;
let base_path = base_path_buf.as_path();
@@ -845,12 +850,13 @@ pub async fn open_task_attempt_in_editor(
config.editor.with_override(editor_type_str)
};
- match editor_config.open_file(&path.to_string_lossy()) {
- Ok(_) => {
+ match editor_config.open_file(path.as_path()) {
+ Ok(url) => {
tracing::info!(
- "Opened editor for task attempt {} at path: {}",
+ "Opened editor for task attempt {} at path: {}{}",
task_attempt.id,
- path.display()
+ path.display(),
+ if url.is_some() { " (remote mode)" } else { "" }
);
deployment
@@ -859,11 +865,14 @@ pub async fn open_task_attempt_in_editor(
serde_json::json!({
"attempt_id": task_attempt.id.to_string(),
"editor_type": payload.as_ref().and_then(|req| req.editor_type.as_ref()),
+ "remote_mode": url.is_some(),
}),
)
.await;
- Ok(ResponseJson(ApiResponse::success(())))
+ Ok(ResponseJson(ApiResponse::success(OpenEditorResponse {
+ url,
+ })))
}
Err(e) => {
tracing::error!(
diff --git a/crates/services/src/services/config/versions/v2.rs b/crates/services/src/services/config/versions/v2.rs
index 9f6eda66..07ba7a83 100644
--- a/crates/services/src/services/config/versions/v2.rs
+++ b/crates/services/src/services/config/versions/v2.rs
@@ -1,4 +1,9 @@
-use std::{path::PathBuf, str::FromStr};
+use std::{
+ io,
+ path::{Path, PathBuf},
+ process::Command,
+ str::FromStr,
+};
use anyhow::Error;
use serde::{Deserialize, Serialize};
@@ -269,6 +274,10 @@ impl From for SoundFile {
pub struct EditorConfig {
editor_type: EditorType,
custom_command: Option,
+ #[serde(default)]
+ remote_ssh_host: Option,
+ #[serde(default)]
+ remote_ssh_user: Option,
}
impl From for EditorConfig {
@@ -276,6 +285,8 @@ impl From for EditorConfig {
Self {
editor_type: EditorType::from(old.editor_type), // Now SCREAMING_SNAKE_CASE
custom_command: old.custom_command,
+ remote_ssh_host: None,
+ remote_ssh_user: None,
}
}
}
@@ -312,6 +323,8 @@ impl Default for EditorConfig {
Self {
editor_type: EditorType::VsCode,
custom_command: None,
+ remote_ssh_host: None,
+ remote_ssh_user: None,
}
}
}
@@ -335,29 +348,61 @@ impl EditorConfig {
}
}
- pub fn open_file(&self, path: &str) -> Result<(), std::io::Error> {
- let mut command = self.get_command();
+ pub fn open_file(&self, path: &Path) -> Result, io::Error> {
+ if let Some(url) = self.remote_url(path) {
+ return Ok(Some(url));
+ }
+ self.spawn_local(path)?;
+ Ok(None)
+ }
+ fn remote_url(&self, path: &Path) -> Option {
+ let remote_host = self.remote_ssh_host.as_ref()?;
+ let scheme = match self.editor_type {
+ EditorType::VsCode => "vscode",
+ EditorType::Cursor => "cursor",
+ EditorType::Windsurf => "windsurf",
+ _ => return None,
+ };
+ let user_part = self
+ .remote_ssh_user
+ .as_ref()
+ .map(|u| format!("{u}@"))
+ .unwrap_or_default();
+ // files must contain a line and column number
+ let line_col = if path.is_file() { ":1:1" } else { "" };
+ let path = path.to_string_lossy();
+ Some(format!(
+ "{scheme}://vscode-remote/ssh-remote+{user_part}{remote_host}{path}{line_col}"
+ ))
+ }
+
+ fn spawn_local(&self, path: &Path) -> Result<(), io::Error> {
+ let command = self.get_command();
if command.is_empty() {
- return Err(std::io::Error::new(
- std::io::ErrorKind::InvalidInput,
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
"No editor command configured",
));
}
- if cfg!(windows) {
- command[0] =
- utils::shell::resolve_executable_path(&command[0]).ok_or(std::io::Error::new(
- std::io::ErrorKind::NotFound,
+ // Resolve the executable path without mutating the vector.
+ let executable = {
+ #[cfg(windows)]
+ {
+ utils::shell::resolve_executable_path(&command[0]).ok_or(io::Error::new(
+ io::ErrorKind::NotFound,
format!("Editor command '{}' not found", command[0]),
- ))?;
- }
+ ))?
+ }
+ #[cfg(not(windows))]
+ {
+ command[0].clone()
+ }
+ };
- let mut cmd = std::process::Command::new(&command[0]);
- for arg in &command[1..] {
- cmd.arg(arg);
- }
- cmd.arg(path);
+ let mut cmd = Command::new(executable);
+ cmd.args(&command[1..]).arg(path);
cmd.spawn()?;
Ok(())
}
@@ -369,6 +414,8 @@ impl EditorConfig {
EditorConfig {
editor_type,
custom_command: self.custom_command.clone(),
+ remote_ssh_host: self.remote_ssh_host.clone(),
+ remote_ssh_user: self.remote_ssh_user.clone(),
}
} else {
self.clone()
diff --git a/docs/configuration-customisation/global-settings.mdx b/docs/configuration-customisation/global-settings.mdx
index ef13a8dc..a868cb62 100644
--- a/docs/configuration-customisation/global-settings.mdx
+++ b/docs/configuration-customisation/global-settings.mdx
@@ -27,7 +27,66 @@ You can override the default agent configuration per attempt in the create attem
## Editor Integration
-Configure integration with your preferred code editor (e.g., VS Code) for a seamless development workflow.
+Configure integration with your preferred code editor for a seamless development workflow.
+
+### Selecting Your Editor
+
+Choose from various supported editors:
+- **VS Code** - Microsoft's popular code editor
+- **Cursor** - VSCode fork with AI-native features
+- **Windsurf** - VSCode fork optimized for collaborative development
+- **Neovim**, **Emacs**, **Sublime Text** - Other popular editors
+- **Custom** - Use a custom shell command
+
+### Remote SSH Configuration
+
+
+
+
+
+When running Vibe Kanban on a remote server (e.g., accessed via Cloudflare tunnel, ngrok, or as a systemctl service), you can configure VSCode-based editors to open projects via SSH instead of assuming localhost.
+
+This feature is available for **VS Code**, **Cursor**, and **Windsurf** editors.
+
+#### When to Use Remote SSH
+
+Enable remote SSH configuration when:
+- Vibe Kanban runs on a remote server (VPS, cloud instance, etc.)
+- You access the web UI through a tunnel or reverse proxy
+- Your code files are on a different machine than your browser
+- You want your local editor to connect to the remote server via SSH
+
+#### Configuration Fields
+
+1. **Remote SSH Host** (Optional)
+ - The hostname or IP address of your remote server
+ - Examples: `example.com`, `192.168.1.100`, `my-server`
+ - Must be accessible via SSH from your local machine
+
+2. **Remote SSH User** (Optional)
+ - The SSH username for connecting to the remote server
+ - If not specified, SSH will use your default user or SSH config
+
+#### How It Works
+
+When remote SSH is configured, clicking "Open in VSCode" (or Cursor/Windsurf):
+1. Generates a special protocol URL like: `vscode://vscode-remote/ssh-remote+user@host/path/to/project`
+2. Opens in your default browser, which launches your local editor
+3. Your editor connects to the remote server via SSH
+4. The project or task worktree opens in the remote context
+
+This works for both project-level and task worktree opening.
+
+#### Prerequisites
+
+- SSH access configured between your local machine and remote server
+- SSH keys or credentials set up (no password prompts)
+- VSCode Remote-SSH extension installed (or equivalent for Cursor/Windsurf)
+- The remote server path must be accessible via SSH
+
+
+Test your SSH connection first with `ssh user@host` to ensure it works without prompting for passwords.
+
## GitHub Integration
diff --git a/docs/images/vk-editor-ssh.png b/docs/images/vk-editor-ssh.png
new file mode 100644
index 00000000..95253d92
Binary files /dev/null and b/docs/images/vk-editor-ssh.png differ
diff --git a/frontend/package.json b/frontend/package.json
index 3c534e17..ba1a0fe9 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -101,4 +101,4 @@
"typescript": "^5.9.2",
"vite": "^5.0.8"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/DiffCard.tsx b/frontend/src/components/DiffCard.tsx
index a85e5f85..9b5dac4d 100644
--- a/frontend/src/components/DiffCard.tsx
+++ b/frontend/src/components/DiffCard.tsx
@@ -239,11 +239,16 @@ export default function DiffCard({
if (!selectedAttempt?.id) return;
try {
const openPath = newName || oldName;
- await attemptsApi.openEditor(
+ const response = await attemptsApi.openEditor(
selectedAttempt.id,
undefined,
openPath || undefined
);
+
+ // If a URL is returned, open it in a new window/tab
+ if (response.url) {
+ window.open(response.url, '_blank');
+ }
} catch (err) {
console.error('Failed to open file in IDE:', err);
}
diff --git a/frontend/src/components/dialogs/global/OnboardingDialog.tsx b/frontend/src/components/dialogs/global/OnboardingDialog.tsx
index 138b0e33..1c5f8381 100644
--- a/frontend/src/components/dialogs/global/OnboardingDialog.tsx
+++ b/frontend/src/components/dialogs/global/OnboardingDialog.tsx
@@ -25,7 +25,7 @@ import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Sparkles, Code, ChevronDown, HandMetal } from 'lucide-react';
import { BaseCodingAgent, EditorType } from 'shared/types';
-import type { ExecutorProfileId } from 'shared/types';
+import type { EditorConfig, ExecutorProfileId } from 'shared/types';
import { useUserSystem } from '@/components/config-provider';
import { toPrettyCase } from '@/utils/string';
@@ -33,7 +33,7 @@ import NiceModal, { useModal } from '@ebay/nice-modal-react';
export type OnboardingResult = {
profile: ExecutorProfileId;
- editor: { editor_type: EditorType; custom_command: string | null };
+ editor: EditorConfig;
};
const OnboardingDialog = NiceModal.create(() => {
@@ -56,6 +56,8 @@ const OnboardingDialog = NiceModal.create(() => {
editor_type: editorType,
custom_command:
editorType === EditorType.CUSTOM ? customCommand || null : null,
+ remote_ssh_host: null,
+ remote_ssh_user: null,
},
} as OnboardingResult);
};
diff --git a/frontend/src/hooks/useOpenInEditor.ts b/frontend/src/hooks/useOpenInEditor.ts
index cb44a291..d8062483 100644
--- a/frontend/src/hooks/useOpenInEditor.ts
+++ b/frontend/src/hooks/useOpenInEditor.ts
@@ -19,21 +19,15 @@ export function useOpenInEditor(
const { editorType, filePath } = options ?? {};
try {
- const result = await attemptsApi.openEditor(
+ const response = await attemptsApi.openEditor(
attemptId,
editorType,
filePath
);
- if (result === undefined && !editorType) {
- if (onShowEditorDialog) {
- onShowEditorDialog();
- } else {
- NiceModal.show('editor-selection', {
- selectedAttemptId: attemptId,
- filePath,
- });
- }
+ // If a URL is returned, open it in a new window/tab
+ if (response.url) {
+ window.open(response.url, '_blank');
}
} catch (err) {
console.error('Failed to open editor:', err);
diff --git a/frontend/src/hooks/useOpenProjectInEditor.ts b/frontend/src/hooks/useOpenProjectInEditor.ts
index 31f145a5..667837fc 100644
--- a/frontend/src/hooks/useOpenProjectInEditor.ts
+++ b/frontend/src/hooks/useOpenProjectInEditor.ts
@@ -12,7 +12,12 @@ export function useOpenProjectInEditor(
if (!project) return;
try {
- await projectsApi.openEditor(project.id, editorType);
+ const response = await projectsApi.openEditor(project.id, editorType);
+
+ // If a URL is returned, open it in a new window/tab
+ if (response.url) {
+ window.open(response.url, '_blank');
+ }
} catch (err) {
console.error('Failed to open project in editor:', err);
if (!editorType) {
diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json
index 0e90f29b..60a38ceb 100644
--- a/frontend/src/i18n/locales/en/settings.json
+++ b/frontend/src/i18n/locales/en/settings.json
@@ -58,7 +58,20 @@
},
"customCommand": {
"label": "Custom Editor Command",
+ "placeholder": "e.g., code, subl, vim",
"helper": "Enter the command to launch your custom editor. This will be used to open files."
+ },
+ "remoteSsh": {
+ "host": {
+ "label": "Remote SSH Host (Optional)",
+ "placeholder": "e.g., hostname or IP address",
+ "helper": "Set this if Vibe Kanban is running on a remote server. When set, clicking \"Open in Editor\" will generate a URL to open your editor via SSH instead of spawning a local command."
+ },
+ "user": {
+ "label": "Remote SSH User (Optional)",
+ "placeholder": "e.g., username",
+ "helper": "SSH username for the remote connection. If not set, VS Code will use your SSH config or prompt you."
+ }
}
},
"github": {
@@ -71,6 +84,7 @@
"or": "OR",
"pat": {
"label": "Personal Access Token",
+ "placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx",
"helper": "GitHub Personal Access Token with 'repo' permissions. Use this if OAuth permissions are insufficient for private repositories and organisation owned repositories.",
"createTokenLink": "Create token here"
}
diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json
index 159c9ea8..ad8c8a20 100644
--- a/frontend/src/i18n/locales/es/settings.json
+++ b/frontend/src/i18n/locales/es/settings.json
@@ -58,7 +58,20 @@
},
"customCommand": {
"label": "Comando de Editor Personalizado",
+ "placeholder": "ej., code, subl, vim",
"helper": "Ingresa el comando para lanzar tu editor personalizado. Se utilizará para abrir archivos."
+ },
+ "remoteSsh": {
+ "host": {
+ "label": "Host SSH Remoto (Opcional)",
+ "placeholder": "ej., nombre de host o dirección IP",
+ "helper": "Configura esto si Vibe Kanban se ejecuta en un servidor remoto. Cuando se establece, al hacer clic en \"Abrir en Editor\" se generará una URL para abrir tu editor a través de SSH en lugar de ejecutar un comando local."
+ },
+ "user": {
+ "label": "Usuario SSH Remoto (Opcional)",
+ "placeholder": "ej., nombre de usuario",
+ "helper": "Nombre de usuario SSH para la conexión remota. Si no se establece, VS Code usará tu configuración SSH o te lo pedirá."
+ }
}
},
"github": {
@@ -71,6 +84,7 @@
"or": "O",
"pat": {
"label": "Token de Acceso Personal",
+ "placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx",
"helper": "Token de Acceso Personal de GitHub con permisos 'repo'. Úsalo si los permisos OAuth son insuficientes para repositorios privados y repositorios de organizaciones.",
"createTokenLink": "Crear token aquí"
}
diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json
index 372d003c..a66d95ab 100644
--- a/frontend/src/i18n/locales/ja/settings.json
+++ b/frontend/src/i18n/locales/ja/settings.json
@@ -58,7 +58,20 @@
},
"customCommand": {
"label": "カスタムエディターコマンド",
+ "placeholder": "例: code, subl, vim",
"helper": "カスタムエディターを起動するコマンドを入力してください。ファイルを開くために使用されます。"
+ },
+ "remoteSsh": {
+ "host": {
+ "label": "リモートSSHホスト(オプション)",
+ "placeholder": "例: ホスト名またはIPアドレス",
+ "helper": "Vibe Kanbanがリモートサーバーで実行されている場合に設定してください。設定すると、「エディターで開く」をクリックしたときに、ローカルコマンドを実行する代わりにSSH経由でエディターを開くURLが生成されます。"
+ },
+ "user": {
+ "label": "リモートSSHユーザー(オプション)",
+ "placeholder": "例: ユーザー名",
+ "helper": "リモート接続のSSHユーザー名。設定されていない場合、VS CodeはSSH設定を使用するか、入力を求めます。"
+ }
}
},
"github": {
@@ -71,6 +84,7 @@
"or": "または",
"pat": {
"label": "個人用アクセストークン",
+ "placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx",
"helper": "「repo」権限を持つGitHub個人用アクセストークン。OAuthの権限がプライベートリポジトリや組織所有のリポジトリに対して不十分な場合に使用してください。",
"createTokenLink": "ここでトークンを作成"
}
diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json
index f857de78..a863ae62 100644
--- a/frontend/src/i18n/locales/ko/settings.json
+++ b/frontend/src/i18n/locales/ko/settings.json
@@ -58,7 +58,20 @@
},
"customCommand": {
"label": "사용자 정의 에디터 명령",
+ "placeholder": "예: code, subl, vim",
"helper": "사용자 정의 에디터를 실행하는 명령을 입력하세요. 파일을 여는 데 사용됩니다."
+ },
+ "remoteSsh": {
+ "host": {
+ "label": "원격 SSH 호스트 (선택사항)",
+ "placeholder": "예: 호스트 이름 또는 IP 주소",
+ "helper": "Vibe Kanban이 원격 서버에서 실행 중인 경우 설정하세요. 설정하면 \"에디터에서 열기\"를 클릭할 때 로컬 명령을 실행하는 대신 SSH를 통해 에디터를 여는 URL이 생성됩니다."
+ },
+ "user": {
+ "label": "원격 SSH 사용자 (선택사항)",
+ "placeholder": "예: 사용자 이름",
+ "helper": "원격 연결을 위한 SSH 사용자 이름입니다. 설정하지 않으면 VS Code가 SSH 설정을 사용하거나 입력을 요청합니다."
+ }
}
},
"github": {
@@ -71,6 +84,7 @@
"or": "또는",
"pat": {
"label": "개인 액세스 토큰",
+ "placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx",
"helper": "'repo' 권한이 있는 GitHub 개인 액세스 토큰입니다. OAuth 권한이 비공개 저장소 및 조직 소유 저장소에 충분하지 않은 경우 사용하세요.",
"createTokenLink": "여기에서 토큰 생성"
}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 8f184aeb..f867fedb 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -91,6 +91,10 @@ export interface FollowUpResponse {
created_new_attempt: boolean;
}
+export interface OpenEditorResponse {
+ url: string | null;
+}
+
export type Ok = { success: true; data: T };
export type Err = { success: false; error: E | undefined; message?: string };
@@ -232,7 +236,10 @@ export const projectsApi = {
return handleApiResponse(response);
},
- openEditor: async (id: string, editorType?: EditorType): Promise => {
+ openEditor: async (
+ id: string,
+ editorType?: EditorType
+ ): Promise => {
const requestBody: any = {};
if (editorType) requestBody.editor_type = editorType;
@@ -242,7 +249,7 @@ export const projectsApi = {
Object.keys(requestBody).length > 0 ? requestBody : null
),
});
- return handleApiResponse(response);
+ return handleApiResponse(response);
},
getBranches: async (id: string): Promise => {
@@ -455,7 +462,7 @@ export const attemptsApi = {
attemptId: string,
editorType?: EditorType,
filePath?: string
- ): Promise => {
+ ): Promise => {
const requestBody: { editor_type?: EditorType; file_path?: string } = {};
if (editorType) requestBody.editor_type = editorType;
if (filePath) requestBody.file_path = filePath;
@@ -469,7 +476,7 @@ export const attemptsApi = {
),
}
);
- return handleApiResponse(response);
+ return handleApiResponse(response);
},
getBranchStatus: async (attemptId: string): Promise => {
diff --git a/frontend/src/pages/settings/GeneralSettings.tsx b/frontend/src/pages/settings/GeneralSettings.tsx
index 3486106c..53ddd3f5 100644
--- a/frontend/src/pages/settings/GeneralSettings.tsx
+++ b/frontend/src/pages/settings/GeneralSettings.tsx
@@ -374,7 +374,8 @@ export function GeneralSettings() {
className="w-full h-10 px-2 flex items-center justify-between"
>
- {currentProfileVariant?.variant || 'DEFAULT'}
+ {currentProfileVariant?.variant ||
+ t('settings.general.taskExecution.defaultLabel')}
@@ -475,7 +476,9 @@ export function GeneralSettings() {
updateDraft({
@@ -491,6 +494,62 @@ export function GeneralSettings() {
)}
+
+ {(draft?.editor.editor_type === EditorType.VS_CODE ||
+ draft?.editor.editor_type === EditorType.CURSOR ||
+ draft?.editor.editor_type === EditorType.WINDSURF) && (
+ <>
+
+
+ {t('settings.general.editor.remoteSsh.host.label')}
+
+
+ updateDraft({
+ editor: {
+ ...draft!.editor,
+ remote_ssh_host: e.target.value || null,
+ },
+ })
+ }
+ />
+
+ {t('settings.general.editor.remoteSsh.host.helper')}
+
+
+
+ {draft?.editor.remote_ssh_host && (
+
+
+ {t('settings.general.editor.remoteSsh.user.label')}
+
+
+ updateDraft({
+ editor: {
+ ...draft!.editor,
+ remote_ssh_user: e.target.value || null,
+ },
+ })
+ }
+ />
+
+ {t('settings.general.editor.remoteSsh.user.helper')}
+
+
+ )}
+ >
+ )}
@@ -550,7 +609,7 @@ export function GeneralSettings() {
updateDraft({
diff --git a/shared/types.ts b/shared/types.ts
index 4ced17e1..bffc0ac6 100644
--- a/shared/types.ts
+++ b/shared/types.ts
@@ -100,7 +100,7 @@ export type NotificationConfig = { sound_enabled: boolean, push_enabled: boolean
export enum ThemeMode { LIGHT = "LIGHT", DARK = "DARK", SYSTEM = "SYSTEM" }
-export type EditorConfig = { editor_type: EditorType, custom_command: string | null, };
+export type EditorConfig = { editor_type: EditorType, custom_command: string | null, remote_ssh_host: string | null, remote_ssh_user: string | null, };
export enum EditorType { VS_CODE = "VS_CODE", CURSOR = "CURSOR", WINDSURF = "WINDSURF", INTELLI_J = "INTELLI_J", ZED = "ZED", XCODE = "XCODE", CUSTOM = "CUSTOM" }