Remote host + username opening for VSCode based IDEs (#1134)
* feat: add remote VSCode SSH support for remote server deployments Add support for opening VSCode via SSH when Vibe Kanban is running on a remote server. This allows users accessing the web UI through a tunnel to open projects/tasks in their local VSCode which connects to the remote server via SSH. Backend changes: - Add remote_ssh_host and remote_ssh_user fields to EditorConfig (v3) - Create config v8 with migration from v7 - Modify EditorConfig.open_file() to return URL when remote mode is enabled - Update API routes to return OpenEditorResponse with optional URL - Generate vscode:// URL scheme for remote SSH connections Frontend changes: - Update API client to handle OpenEditorResponse type - Modify hooks to open URLs in new tabs when returned - Add UI fields in settings for remote SSH configuration When remote_ssh_host is configured, clicking "Open in VSCode" generates a URL like: vscode://vscode-remote/ssh-remote+user@host/path/to/project This works for both project-level and task-level editor opening. * feat: extend remote SSH support to Cursor and Windsurf editors Extend the remote SSH feature to support Cursor and Windsurf editors, which are VSCode forks that use the same remote SSH protocol. Changes: - Update EditorConfig.open_file() to generate cursor:// and windsurf:// URLs - Show SSH configuration fields in settings for all three VSCode-based editors - Use same vscode-remote SSH URL pattern for all three editors When remote_ssh_host is configured, supported editors generate URLs like: - vscode://vscode-remote/ssh-remote+user@host/path - cursor://vscode-remote/ssh-remote+user@host/path - windsurf://vscode-remote/ssh-remote+user@host/path * fix: address clippy linting issues - Fix uninlined format args in v3.rs - Add allow attributes for re-exported types in v5-v7 * docs: add remote SSH configuration documentation Add comprehensive documentation for the remote SSH editor feature: - Expand Editor Integration section in global-settings.mdx - Document when to use remote SSH (tunnels, systemctl services, remote deployments) - Explain configuration fields (remote_ssh_host, remote_ssh_user) - Detail how the feature works with protocol URLs - List prerequisites for SSH access and VSCode Remote-SSH extension - Support for VSCode, Cursor, and Windsurf editors - Add Remote Deployment section to README.md - Quick guide for setting up remote SSH access - Link to detailed documentation - Include in feature list This documentation helps users understand and configure the remote SSH feature when running Vibe Kanban on remote servers accessed via browser. * remove package-lock in favour of pnpm lock * rollback config version increment * re-impl remote URL * Update i18n for general settings (vibe-kanban 4a1a3ae1) frontend/src/pages/settings/GeneralSettings.tsx Find any strings here that haven't been i18n'd and i18n them * add line number/col to the end of vscode-remote file paths * handle response url when opening file in ide from DiffCard * update remote-ssh guidance in readme * add image to global settings docs --------- Co-authored-by: Stephan Fitzpatrick <stephan@knowsuchagency.com> Co-authored-by: Britannio Jarrett <britanniojarrett@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e7cc1163bc
commit
e8ff40d5a4
18
README.md
18
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.
|
||||
|
||||
@@ -264,12 +264,17 @@ pub struct OpenEditorRequest {
|
||||
editor_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, ts_rs::TS)]
|
||||
pub struct OpenEditorResponse {
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn open_project_in_editor(
|
||||
Extension(project): Extension<Project>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Json(payload): Json<Option<OpenEditorRequest>>,
|
||||
) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
|
||||
let path = project.git_repo_path.to_string_lossy();
|
||||
) -> Result<ResponseJson<ApiResponse<OpenEditorResponse>>, 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);
|
||||
|
||||
@@ -823,11 +823,16 @@ pub struct OpenEditorRequest {
|
||||
file_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, TS)]
|
||||
pub struct OpenEditorResponse {
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn open_task_attempt_in_editor(
|
||||
Extension(task_attempt): Extension<TaskAttempt>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Json(payload): Json<Option<OpenEditorRequest>>,
|
||||
) -> Result<ResponseJson<ApiResponse<()>>, ApiError> {
|
||||
) -> Result<ResponseJson<ApiResponse<OpenEditorResponse>>, 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!(
|
||||
|
||||
@@ -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<v1::SoundFile> for SoundFile {
|
||||
pub struct EditorConfig {
|
||||
editor_type: EditorType,
|
||||
custom_command: Option<String>,
|
||||
#[serde(default)]
|
||||
remote_ssh_host: Option<String>,
|
||||
#[serde(default)]
|
||||
remote_ssh_user: Option<String>,
|
||||
}
|
||||
|
||||
impl From<v1::EditorConfig> for EditorConfig {
|
||||
@@ -276,6 +285,8 @@ impl From<v1::EditorConfig> 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<Option<String>, 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<String> {
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
<Frame>
|
||||
<img src="/images/vk-editor-ssh.png" alt="Vibe Kanban settings editor section showing ssh configuration options." />
|
||||
</Frame>
|
||||
|
||||
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
|
||||
|
||||
<Tip>
|
||||
Test your SSH connection first with `ssh user@host` to ensure it works without prompting for passwords.
|
||||
</Tip>
|
||||
|
||||
## GitHub Integration
|
||||
|
||||
|
||||
BIN
docs/images/vk-editor-ssh.png
Normal file
BIN
docs/images/vk-editor-ssh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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í"
|
||||
}
|
||||
|
||||
@@ -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": "ここでトークンを作成"
|
||||
}
|
||||
|
||||
@@ -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": "여기에서 토큰 생성"
|
||||
}
|
||||
|
||||
@@ -91,6 +91,10 @@ export interface FollowUpResponse {
|
||||
created_new_attempt: boolean;
|
||||
}
|
||||
|
||||
export interface OpenEditorResponse {
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export type Ok<T> = { success: true; data: T };
|
||||
export type Err<E> = { success: false; error: E | undefined; message?: string };
|
||||
|
||||
@@ -232,7 +236,10 @@ export const projectsApi = {
|
||||
return handleApiResponse<void>(response);
|
||||
},
|
||||
|
||||
openEditor: async (id: string, editorType?: EditorType): Promise<void> => {
|
||||
openEditor: async (
|
||||
id: string,
|
||||
editorType?: EditorType
|
||||
): Promise<OpenEditorResponse> => {
|
||||
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<void>(response);
|
||||
return handleApiResponse<OpenEditorResponse>(response);
|
||||
},
|
||||
|
||||
getBranches: async (id: string): Promise<GitBranch[]> => {
|
||||
@@ -455,7 +462,7 @@ export const attemptsApi = {
|
||||
attemptId: string,
|
||||
editorType?: EditorType,
|
||||
filePath?: string
|
||||
): Promise<void> => {
|
||||
): Promise<OpenEditorResponse> => {
|
||||
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<void>(response);
|
||||
return handleApiResponse<OpenEditorResponse>(response);
|
||||
},
|
||||
|
||||
getBranchStatus: async (attemptId: string): Promise<BranchStatus> => {
|
||||
|
||||
@@ -374,7 +374,8 @@ export function GeneralSettings() {
|
||||
className="w-full h-10 px-2 flex items-center justify-between"
|
||||
>
|
||||
<span className="text-sm truncate flex-1 text-left">
|
||||
{currentProfileVariant?.variant || 'DEFAULT'}
|
||||
{currentProfileVariant?.variant ||
|
||||
t('settings.general.taskExecution.defaultLabel')}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 ml-1 flex-shrink-0" />
|
||||
</Button>
|
||||
@@ -475,7 +476,9 @@ export function GeneralSettings() {
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-command"
|
||||
placeholder="e.g., code, subl, vim"
|
||||
placeholder={t(
|
||||
'settings.general.editor.customCommand.placeholder'
|
||||
)}
|
||||
value={draft?.editor.custom_command || ''}
|
||||
onChange={(e) =>
|
||||
updateDraft({
|
||||
@@ -491,6 +494,62 @@ export function GeneralSettings() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(draft?.editor.editor_type === EditorType.VS_CODE ||
|
||||
draft?.editor.editor_type === EditorType.CURSOR ||
|
||||
draft?.editor.editor_type === EditorType.WINDSURF) && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="remote-ssh-host">
|
||||
{t('settings.general.editor.remoteSsh.host.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="remote-ssh-host"
|
||||
placeholder={t(
|
||||
'settings.general.editor.remoteSsh.host.placeholder'
|
||||
)}
|
||||
value={draft?.editor.remote_ssh_host || ''}
|
||||
onChange={(e) =>
|
||||
updateDraft({
|
||||
editor: {
|
||||
...draft!.editor,
|
||||
remote_ssh_host: e.target.value || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings.general.editor.remoteSsh.host.helper')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{draft?.editor.remote_ssh_host && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="remote-ssh-user">
|
||||
{t('settings.general.editor.remoteSsh.user.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="remote-ssh-user"
|
||||
placeholder={t(
|
||||
'settings.general.editor.remoteSsh.user.placeholder'
|
||||
)}
|
||||
value={draft?.editor.remote_ssh_user || ''}
|
||||
onChange={(e) =>
|
||||
updateDraft({
|
||||
editor: {
|
||||
...draft!.editor,
|
||||
remote_ssh_user: e.target.value || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings.general.editor.remoteSsh.user.helper')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -550,7 +609,7 @@ export function GeneralSettings() {
|
||||
<Input
|
||||
id="github-token"
|
||||
type="password"
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
placeholder={t('settings.general.github.pat.placeholder')}
|
||||
value={draft?.github.pat || ''}
|
||||
onChange={(e) =>
|
||||
updateDraft({
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user