Merge branch: resolve conflict in worktree-actions-dropdown.tsx

This commit is contained in:
Kacper
2026-01-11 20:08:19 +01:00
118 changed files with 6327 additions and 1795 deletions

View File

@@ -6,13 +6,15 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
} from '@/components/ui/dropdown-menu';
import {
Trash2,
MoreHorizontal,
GitCommit,
GitPullRequest,
ExternalLink,
Download,
Upload,
Play,
@@ -22,15 +24,18 @@ import {
GitMerge,
AlertCircle,
RefreshCw,
Copy,
} from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import type { WorktreeInfo, DevServerInfo, PRInfo, GitRepoStatus } from '../types';
import { TooltipWrapper } from './tooltip-wrapper';
import { useAvailableEditors, useEffectiveDefaultEditor } from '../hooks/use-available-editors';
import { getEditorIcon } from '@/components/icons/editor-icons';
interface WorktreeActionsDropdownProps {
worktree: WorktreeInfo;
isSelected: boolean;
defaultEditorName: string;
aheadCount: number;
behindCount: number;
isPulling: boolean;
@@ -42,7 +47,7 @@ interface WorktreeActionsDropdownProps {
onOpenChange: (open: boolean) => void;
onPull: (worktree: WorktreeInfo) => void;
onPush: (worktree: WorktreeInfo) => void;
onOpenInEditor: (worktree: WorktreeInfo) => void;
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
@@ -58,7 +63,6 @@ interface WorktreeActionsDropdownProps {
export function WorktreeActionsDropdown({
worktree,
isSelected,
defaultEditorName,
aheadCount,
behindCount,
isPulling,
@@ -82,6 +86,20 @@ export function WorktreeActionsDropdown({
onRunInitScript,
hasInitScript,
}: WorktreeActionsDropdownProps) {
// Get available editors for the "Open In" submenu
const { editors } = useAvailableEditors();
// Use shared hook for effective default editor
const effectiveDefaultEditor = useEffectiveDefaultEditor(editors);
// Get other editors (excluding the default) for the submenu
const otherEditors = editors.filter((e) => e.command !== effectiveDefaultEditor?.command);
// Get icon component for the effective editor (avoid IIFE in JSX)
const DefaultEditorIcon = effectiveDefaultEditor
? getEditorIcon(effectiveDefaultEditor.command)
: null;
// Check if there's a PR associated with this worktree from stored metadata
const hasPR = !!worktree.pr;
@@ -205,10 +223,54 @@ export function WorktreeActionsDropdown({
</TooltipWrapper>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => onOpenInEditor(worktree)} className="text-xs">
<ExternalLink className="w-3.5 h-3.5 mr-2" />
Open in {defaultEditorName}
</DropdownMenuItem>
{/* Open in editor - split button: click main area for default, chevron for other options */}
{effectiveDefaultEditor && (
<DropdownMenuSub>
<div className="flex items-center">
{/* Main clickable area - opens in default editor */}
<DropdownMenuItem
onClick={() => onOpenInEditor(worktree, effectiveDefaultEditor.command)}
className="text-xs flex-1 pr-0 rounded-r-none"
>
{DefaultEditorIcon && <DefaultEditorIcon className="w-3.5 h-3.5 mr-2" />}
Open in {effectiveDefaultEditor.name}
</DropdownMenuItem>
{/* Chevron trigger for submenu with other editors and Copy Path */}
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
</div>
<DropdownMenuSubContent>
{/* Other editors */}
{otherEditors.map((editor) => {
const EditorIcon = getEditorIcon(editor.command);
return (
<DropdownMenuItem
key={editor.command}
onClick={() => onOpenInEditor(worktree, editor.command)}
className="text-xs"
>
<EditorIcon className="w-3.5 h-3.5 mr-2" />
{editor.name}
</DropdownMenuItem>
);
})}
{otherEditors.length > 0 && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={async () => {
try {
await navigator.clipboard.writeText(worktree.path);
toast.success('Path copied to clipboard');
} catch {
toast.error('Failed to copy path to clipboard');
}
}}
className="text-xs"
>
<Copy className="w-3.5 h-3.5 mr-2" />
Copy Path
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
)}
{!worktree.isMain && hasInitScript && (
<DropdownMenuItem onClick={() => onRunInitScript(worktree)} className="text-xs">
<RefreshCw className="w-3.5 h-3.5 mr-2" />

View File

@@ -17,7 +17,6 @@ interface WorktreeTabProps {
isActivating: boolean;
isDevServerRunning: boolean;
devServerInfo?: DevServerInfo;
defaultEditorName: string;
branches: BranchInfo[];
filteredBranches: BranchInfo[];
branchFilter: string;
@@ -37,7 +36,7 @@ interface WorktreeTabProps {
onCreateBranch: (worktree: WorktreeInfo) => void;
onPull: (worktree: WorktreeInfo) => void;
onPush: (worktree: WorktreeInfo) => void;
onOpenInEditor: (worktree: WorktreeInfo) => void;
onOpenInEditor: (worktree: WorktreeInfo, editorCommand?: string) => void;
onCommit: (worktree: WorktreeInfo) => void;
onCreatePR: (worktree: WorktreeInfo) => void;
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
@@ -60,7 +59,6 @@ export function WorktreeTab({
isActivating,
isDevServerRunning,
devServerInfo,
defaultEditorName,
branches,
filteredBranches,
branchFilter,
@@ -319,7 +317,6 @@ export function WorktreeTab({
<WorktreeActionsDropdown
worktree={worktree}
isSelected={isSelected}
defaultEditorName={defaultEditorName}
aheadCount={aheadCount}
behindCount={behindCount}
isPulling={isPulling}

View File

@@ -2,5 +2,5 @@ export { useWorktrees } from './use-worktrees';
export { useDevServers } from './use-dev-servers';
export { useBranches } from './use-branches';
export { useWorktreeActions } from './use-worktree-actions';
export { useDefaultEditor } from './use-default-editor';
export { useRunningFeatures } from './use-running-features';
export { useAvailableEditors, useEffectiveDefaultEditor } from './use-available-editors';

View File

@@ -0,0 +1,101 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
import type { EditorInfo } from '@automaker/types';
const logger = createLogger('AvailableEditors');
// Re-export EditorInfo for convenience
export type { EditorInfo };
export function useAvailableEditors() {
const [editors, setEditors] = useState<EditorInfo[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const fetchAvailableEditors = useCallback(async () => {
try {
const api = getElectronAPI();
if (!api?.worktree?.getAvailableEditors) {
setIsLoading(false);
return;
}
const result = await api.worktree.getAvailableEditors();
if (result.success && result.result?.editors) {
setEditors(result.result.editors);
}
} catch (error) {
logger.error('Failed to fetch available editors:', error);
} finally {
setIsLoading(false);
}
}, []);
/**
* Refresh editors by clearing the server cache and re-detecting
* Use this when the user has installed/uninstalled editors
*/
const refresh = useCallback(async () => {
setIsRefreshing(true);
try {
const api = getElectronAPI();
if (!api?.worktree?.refreshEditors) {
// Fallback to regular fetch if refresh not available
await fetchAvailableEditors();
return;
}
const result = await api.worktree.refreshEditors();
if (result.success && result.result?.editors) {
setEditors(result.result.editors);
logger.info(`Editor cache refreshed, found ${result.result.editors.length} editors`);
}
} catch (error) {
logger.error('Failed to refresh editors:', error);
} finally {
setIsRefreshing(false);
}
}, [fetchAvailableEditors]);
useEffect(() => {
fetchAvailableEditors();
}, [fetchAvailableEditors]);
return {
editors,
isLoading,
isRefreshing,
refresh,
// Convenience property: has multiple editors (for deciding whether to show submenu)
hasMultipleEditors: editors.length > 1,
// The first editor is the "default" one
defaultEditor: editors[0] ?? null,
};
}
/**
* Hook to get the effective default editor based on user settings
* Falls back to: Cursor > VS Code > first available editor
*/
export function useEffectiveDefaultEditor(editors: EditorInfo[]): EditorInfo | null {
const defaultEditorCommand = useAppStore((s) => s.defaultEditorCommand);
return useMemo(() => {
if (editors.length === 0) return null;
// If user has a saved preference and it exists in available editors, use it
if (defaultEditorCommand) {
const found = editors.find((e) => e.command === defaultEditorCommand);
if (found) return found;
}
// Auto-detect: prefer Cursor, then VS Code, then first available
const cursor = editors.find((e) => e.command === 'cursor');
if (cursor) return cursor;
const vscode = editors.find((e) => e.command === 'code');
if (vscode) return vscode;
return editors[0];
}, [editors, defaultEditorCommand]);
}

View File

@@ -125,14 +125,14 @@ export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktre
[isPushing, fetchBranches, fetchWorktrees]
);
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo) => {
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo, editorCommand?: string) => {
try {
const api = getElectronAPI();
if (!api?.worktree?.openInEditor) {
logger.warn('Open in editor API not available');
return;
}
const result = await api.worktree.openInEditor(worktree.path);
const result = await api.worktree.openInEditor(worktree.path, editorCommand);
if (result.success && result.result) {
toast.success(result.result.message);
} else if (result.error) {

View File

@@ -10,7 +10,6 @@ import {
useDevServers,
useBranches,
useWorktreeActions,
useDefaultEditor,
useRunningFeatures,
} from './hooks';
import { WorktreeTab } from './components';
@@ -77,8 +76,6 @@ export function WorktreePanel({
fetchBranches,
});
const { defaultEditorName } = useDefaultEditor();
const { hasRunningFeatures } = useRunningFeatures({
runningFeatureIds,
features,
@@ -188,7 +185,6 @@ export function WorktreePanel({
isActivating={isActivating}
isDevServerRunning={isDevServerRunning(mainWorktree)}
devServerInfo={getDevServerInfo(mainWorktree)}
defaultEditorName={defaultEditorName}
branches={branches}
filteredBranches={filteredBranches}
branchFilter={branchFilter}
@@ -245,7 +241,6 @@ export function WorktreePanel({
isActivating={isActivating}
isDevServerRunning={isDevServerRunning(worktree)}
devServerInfo={getDevServerInfo(worktree)}
defaultEditorName={defaultEditorName}
branches={branches}
filteredBranches={filteredBranches}
branchFilter={branchFilter}