Show editor availability (vibe-kanban) (#1325)
* Perfect! I've successfully refactored the editor opening code to use the `CommandBuilder` pattern. Here's a summary of the changes: - Added `use executors::command::CommandBuilder;` - Changed return type from `Vec<String>` to `CommandBuilder` - Now returns a proper `CommandBuilder` that handles shell word splitting correctly - Supports quoted strings in custom commands (via `shlex` on Unix, `winsplit` on Windows) - Uses `CommandBuilder` pattern: `build_initial()` → `into_resolved()` - Resolves executables properly on **both Unix and Windows** (previously only Windows had resolution) - Better error messages that show which executable couldn't be found - Maps `ExecutorError` to `io::Error` for consistency with the function signature ✅ **Consistent cross-platform behavior**: Unix now also resolves executables properly ✅ **Sophisticated PATH resolution**: Uses login shell refresh logic for finding executables ✅ **Proper shell word splitting**: Handles quoted strings correctly in custom commands ✅ **Better error messages**: Shows exactly which executable couldn't be found ✅ **Follows established patterns**: Uses the same proven pattern as other executors in the codebase All checks pass: compilation, tests, and Clippy linting. The code is ready! Perfect! All changes have been successfully implemented and tested. Here's what was done: - Changed from: `Result<ResponseJson<ApiResponse<OpenEditorResponse>>, StatusCode>` - Changed to: `Result<ResponseJson<ApiResponse<OpenEditorResponse>>, ApiError>` - Changed from: `Err(StatusCode::INTERNAL_SERVER_ERROR)` (generic 500 error) - Changed to: `Err(ApiError::Io(e))` (passes actual error message) Now when the editor command fails (e.g., executable not found), the API will return a proper error response with the actual error message like: - `"IoError: Editor command not found: Executable 'cursor' not found in PATH"` This matches the pattern already used in `task_attempts.rs` and provides meaningful feedback to the frontend, which can display the error to users or fall back to the editor selection dialog. All checks pass: compilation, TypeScript type checking, and Clippy linting! Perfect! All changes have been successfully implemented and tested. Here's a summary: - New `EditorOpenError` enum with `ExecutableNotFound` variant - Contains structured fields: `executable` (the command that wasn't found) and `editor_type` (the configured editor) - Exports to TypeScript with discriminated union type - Changed `open_file()` return type: `Result<Option<String>, io::Error>` → `Result<Option<String>, EditorOpenError>` - Changed `spawn_local()` return type: `io::Result<()>` → `Result<(), EditorOpenError>` - Maps `ExecutorError::ExecutableNotFound` to structured `EditorOpenError::ExecutableNotFound` - Removed unused `io` import - Added `EditorOpen(#[from] EditorOpenError)` variant - Maps to `StatusCode::BAD_REQUEST` (400) instead of 500 - Returns proper error type: `"EditorOpenError"` - **`crates/server/src/routes/projects.rs:452`**: Changed from `ApiError::Io(e)` to `ApiError::EditorOpen(e)` - **`crates/server/src/routes/task_attempts.rs:965`**: Changed from generic `TaskAttemptError::ValidationError` to `ApiError::EditorOpen(e)` ```typescript export type EditorOpenError = { "type": "executable_not_found", executable: string, editor_type: string, }; ``` Now when an editor executable isn't found, the frontend receives: ```json { "type": "executable_not_found", "executable": "cursor", "editor_type": "CURSOR" } ``` Instead of a generic 500 error. This allows the frontend to: - Show specific error messages ("Cursor not found in PATH") - Suggest alternative editors - Provide installation links or PATH setup instructions - Offer to open the editor settings dialog All checks pass: compilation, TypeScript type checking, Clippy linting, and tests! Cleanup script changes for task attempt f9923e07-9b3b-4d18-b4d1-d4235cf0e12d * Refactor editor config (vibe-kanban a60c079b) Currently edit config is defined in crates/services/src/services/config/versions/v2.rs, we should consider moving it into a module instead. Weigh pros and cons, best practices and the current architecture * Review editor refactor (vibe-kanban 829ba3e5) Review the last 5 commits There's a rebase conflict in `GeneralSettings.tsx` that needs resolution. The conflict is in the imports: **Conflict (lines 41-49):** - **Your branch (HEAD)**: Imports from `ThemeProvider` and `ConfigProvider` (PascalCase) - **Main branch**: Imports from `theme-provider` and `config-provider` (kebab-case) + adds new editor availability features **Resolution needed:** Keep the main branch version because it: 1. Uses the newer kebab-case file naming convention 2. Adds `useEditorAvailability` and `EditorAvailabilityIndicator` features that complement your error handling work The resolved imports should be: ```typescript import { useEditorAvailability } from '@/hooks/useEditorAvailability'; import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator'; import { useTheme } from '@/components/theme-provider'; import { useUserSystem } from '@/components/config-provider'; ``` Would you like me to resolve this conflict and continue the rebase? Show available editors at startup (vibe-kanban 9f1ea85f) When onboarding, show a green checkmark next to installed editors. Use the editor struct refactored in the last 6 commits to make it work. Check eveyer editor in the enum at vk startup. Mkae sure editors being unailable does NOT break anything Fix imports after rebase - use PascalCase for ThemeProvider and ConfigProvider Perfect! All changes complete. Let me create a summary: ✅ **Changes made:** - Added `CheckEditorAvailabilityQuery::decl()` - Added `CheckEditorAvailabilityResponse::decl()` New types exported: ```typescript export type CheckEditorAvailabilityQuery = { editor_type: EditorType }; export type CheckEditorAvailabilityResponse = { available: boolean }; ``` - Added import for `CheckEditorAvailabilityResponse` - Updated `checkEditorAvailability` method: - Parameter type: `string` → `EditorType` (type-safe enum) - Return type: `{ available: boolean }` → `CheckEditorAvailabilityResponse` - ✅ Full type safety for editor availability checking - ✅ Frontend uses typed `EditorType` enum instead of strings - ✅ Consistent with other query/response type patterns - ✅ All checks pass (TypeScript + Rust compilation) The editor availability feature now has complete type safety from backend to frontend! * Fix i18n (vibe-kanban 57188642) For new editory avilability added in the last 3 commits. Run @scripts/check-i18n.sh until it passes. Make sure to check the script and set GITHUB_BASE_REF to main * Fix import: use github not github_service after rebase
This commit is contained in:
48
frontend/src/components/EditorAvailabilityIndicator.tsx
Normal file
48
frontend/src/components/EditorAvailabilityIndicator.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Check, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { EditorAvailabilityState } from '@/hooks/useEditorAvailability';
|
||||
|
||||
interface EditorAvailabilityIndicatorProps {
|
||||
availability: EditorAvailabilityState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual indicator showing whether an editor is available on the system.
|
||||
* Shows loading spinner, green checkmark, or orange warning.
|
||||
*/
|
||||
export function EditorAvailabilityIndicator({
|
||||
availability,
|
||||
}: EditorAvailabilityIndicatorProps) {
|
||||
const { t } = useTranslation('settings');
|
||||
|
||||
if (!availability) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{availability === 'checking' && (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-muted-foreground">
|
||||
{t('settings.general.editor.availability.checking')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{availability === 'available' && (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
<span className="text-green-600">
|
||||
{t('settings.general.editor.availability.available')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{availability === 'unavailable' && (
|
||||
<>
|
||||
<AlertCircle className="h-4 w-4 text-orange-500" />
|
||||
<span className="text-orange-600">
|
||||
{t('settings.general.editor.availability.notFound')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,8 @@ import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { toPrettyCase } from '@/utils/string';
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react';
|
||||
import { defineModal, type NoProps } from '@/lib/modals';
|
||||
import { useEditorAvailability } from '@/hooks/useEditorAvailability';
|
||||
import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator';
|
||||
|
||||
export type OnboardingResult = {
|
||||
profile: ExecutorProfileId;
|
||||
@@ -50,6 +52,9 @@ const OnboardingDialogImpl = NiceModal.create<NoProps>(() => {
|
||||
const [editorType, setEditorType] = useState<EditorType>(EditorType.VS_CODE);
|
||||
const [customCommand, setCustomCommand] = useState<string>('');
|
||||
|
||||
// Check editor availability when selection changes
|
||||
const editorAvailability = useEditorAvailability(editorType);
|
||||
|
||||
const handleComplete = () => {
|
||||
modal.resolve({
|
||||
profile,
|
||||
@@ -192,6 +197,12 @@ const OnboardingDialogImpl = NiceModal.create<NoProps>(() => {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Editor availability status indicator */}
|
||||
{editorType !== EditorType.CUSTOM && (
|
||||
<EditorAvailabilityIndicator availability={editorAvailability} />
|
||||
)}
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This editor will be used to open task attempts and project files.
|
||||
</p>
|
||||
|
||||
44
frontend/src/hooks/useEditorAvailability.ts
Normal file
44
frontend/src/hooks/useEditorAvailability.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { EditorType } from 'shared/types';
|
||||
import { configApi } from '@/lib/api';
|
||||
|
||||
export type EditorAvailabilityState =
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'unavailable'
|
||||
| null;
|
||||
|
||||
/**
|
||||
* Hook to check if an editor is available on the system.
|
||||
* Automatically checks when the editor type changes.
|
||||
* Returns null for Custom editors (can't pre-validate).
|
||||
*/
|
||||
export function useEditorAvailability(
|
||||
editorType: EditorType | null | undefined
|
||||
): EditorAvailabilityState {
|
||||
const [availability, setAvailability] =
|
||||
useState<EditorAvailabilityState>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't check for Custom editor or if no editor type
|
||||
if (!editorType || editorType === EditorType.CUSTOM) {
|
||||
setAvailability(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAvailability = async () => {
|
||||
setAvailability('checking');
|
||||
try {
|
||||
const result = await configApi.checkEditorAvailability(editorType);
|
||||
setAvailability(result.available ? 'available' : 'unavailable');
|
||||
} catch (error) {
|
||||
console.error('Failed to check editor availability:', error);
|
||||
setAvailability(null);
|
||||
}
|
||||
};
|
||||
|
||||
checkAvailability();
|
||||
}, [editorType]);
|
||||
|
||||
return availability;
|
||||
}
|
||||
@@ -74,6 +74,11 @@
|
||||
"placeholder": "e.g., username",
|
||||
"helper": "SSH username for the remote connection. If not set, VS Code will use your SSH config or prompt you."
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"checking": "Checking availability...",
|
||||
"available": "Available",
|
||||
"notFound": "Not found in PATH"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"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á."
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"checking": "Verificando disponibilidad...",
|
||||
"available": "Disponible",
|
||||
"notFound": "No encontrado en PATH"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"placeholder": "例: ユーザー名",
|
||||
"helper": "リモート接続のSSHユーザー名。設定されていない場合、VS CodeはSSH設定を使用するか、入力を求めます。"
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"checking": "利用可能性を確認中...",
|
||||
"available": "利用可能",
|
||||
"notFound": "PATHに見つかりません"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"placeholder": "예: 사용자 이름",
|
||||
"helper": "원격 연결을 위한 SSH 사용자 이름입니다. 설정하지 않으면 VS Code가 SSH 설정을 사용하거나 입력을 요청합니다."
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"checking": "사용 가능 여부 확인 중...",
|
||||
"available": "사용 가능",
|
||||
"notFound": "PATH에서 찾을 수 없음"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
ChangeTargetBranchResponse,
|
||||
RenameBranchRequest,
|
||||
RenameBranchResponse,
|
||||
CheckEditorAvailabilityResponse,
|
||||
RunAgentSetupRequest,
|
||||
RunAgentSetupResponse,
|
||||
GhCliSetupError,
|
||||
@@ -715,6 +716,14 @@ export const configApi = {
|
||||
});
|
||||
return handleApiResponse<Config>(response);
|
||||
},
|
||||
checkEditorAvailability: async (
|
||||
editorType: EditorType
|
||||
): Promise<CheckEditorAvailabilityResponse> => {
|
||||
const response = await makeRequest(
|
||||
`/api/editors/check-availability?editor_type=${encodeURIComponent(editorType)}`
|
||||
);
|
||||
return handleApiResponse<CheckEditorAvailabilityResponse>(response);
|
||||
},
|
||||
};
|
||||
|
||||
// Task Tags APIs (all tags are global)
|
||||
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
import { getLanguageOptions } from '@/i18n/languages';
|
||||
|
||||
import { toPrettyCase } from '@/utils/string';
|
||||
import { useEditorAvailability } from '@/hooks/useEditorAvailability';
|
||||
import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator';
|
||||
import { useTheme } from '@/components/ThemeProvider';
|
||||
import { useUserSystem } from '@/components/ConfigProvider';
|
||||
import { TagManager } from '@/components/TagManager';
|
||||
@@ -70,6 +72,9 @@ export function GeneralSettings() {
|
||||
);
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
// Check editor availability when draft editor changes
|
||||
const editorAvailability = useEditorAvailability(draft?.editor.editor_type);
|
||||
|
||||
const validateBranchPrefix = useCallback(
|
||||
(prefix: string): string | null => {
|
||||
if (!prefix) return null; // empty allowed
|
||||
@@ -447,6 +452,12 @@ export function GeneralSettings() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Editor availability status indicator */}
|
||||
{draft?.editor.editor_type !== EditorType.CUSTOM && (
|
||||
<EditorAvailabilityIndicator availability={editorAvailability} />
|
||||
)}
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('settings.general.editor.type.helper')}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user