mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
Feature: worktree view customization and stability fixes (#805)
* Changes from feature/worktree-view-customization * Feature: Git sync, set-tracking, and push divergence handling (#796) * Add quick-add feature with improved workflows (#802) * Changes from feature/quick-add * feat: Clarify system prompt and improve error handling across services. Address PR Feedback * feat: Improve PR description parsing and refactor event handling * feat: Add context options to pipeline orchestrator initialization * fix: Deduplicate React and handle CJS interop for use-sync-external-store Resolve "Cannot read properties of null (reading 'useState')" errors by deduplicating React/react-dom and ensuring use-sync-external-store is bundled together with React to prevent CJS packages from resolving to different React instances. * Changes from feature/worktree-view-customization * refactor: Remove unused worktree swap and highlight props * refactor: Consolidate feature completion logic and improve thinking level defaults * feat: Increase max turn limit to 10000 - Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts - Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts - Update UI clamping logic from 2000 to 10000 in app-store.ts - Update fallback values from 1000 to 10000 in use-settings-sync.ts - Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS - Update documentation to reflect new range: 1-10000 Allows agents to perform up to 10000 turns for complex feature execution. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> * feat: Add model resolution, improve session handling, and enhance UI stability * refactor: Remove unused sync and tracking branch props from worktree components * feat: Add PR number update functionality to worktrees. Address pr feedback * feat: Optimize Gemini CLI startup and add tool result tracking * refactor: Improve error handling and simplify worktree task cleanup --------- Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,11 @@ import { cn } from '@/lib/utils';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { useAppStore, ThinkingLevel, FeatureImage, PlanningMode, Feature } from '@/store/app-store';
|
||||
import type { ReasoningEffort, PhaseModelEntry, AgentModel } from '@automaker/types';
|
||||
import { supportsReasoningEffort, isAdaptiveThinkingModel } from '@automaker/types';
|
||||
import {
|
||||
supportsReasoningEffort,
|
||||
isAdaptiveThinkingModel,
|
||||
getThinkingLevelsForModel,
|
||||
} from '@automaker/types';
|
||||
import {
|
||||
PrioritySelector,
|
||||
WorkModeSelector,
|
||||
@@ -211,6 +215,7 @@ export function AddFeatureDialog({
|
||||
defaultRequirePlanApproval,
|
||||
useWorktrees,
|
||||
defaultFeatureModel,
|
||||
defaultThinkingLevel,
|
||||
currentProject,
|
||||
} = useAppStore();
|
||||
|
||||
@@ -240,7 +245,22 @@ export function AddFeatureDialog({
|
||||
);
|
||||
setPlanningMode(defaultPlanningMode);
|
||||
setRequirePlanApproval(defaultRequirePlanApproval);
|
||||
setModelEntry(effectiveDefaultFeatureModel);
|
||||
|
||||
// Apply defaultThinkingLevel from settings to the model entry.
|
||||
// This ensures the "Quick-Select Defaults" thinking level setting is respected
|
||||
// even when the user doesn't change the model in the dropdown.
|
||||
const modelId =
|
||||
typeof effectiveDefaultFeatureModel.model === 'string'
|
||||
? effectiveDefaultFeatureModel.model
|
||||
: '';
|
||||
const availableLevels = getThinkingLevelsForModel(modelId);
|
||||
const effectiveThinkingLevel = availableLevels.includes(defaultThinkingLevel)
|
||||
? defaultThinkingLevel
|
||||
: availableLevels[0];
|
||||
setModelEntry({
|
||||
...effectiveDefaultFeatureModel,
|
||||
thinkingLevel: effectiveThinkingLevel,
|
||||
});
|
||||
|
||||
// Initialize description history (empty for new feature)
|
||||
setDescriptionHistory([]);
|
||||
@@ -269,6 +289,7 @@ export function AddFeatureDialog({
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
effectiveDefaultFeatureModel,
|
||||
defaultThinkingLevel,
|
||||
useWorktrees,
|
||||
selectedNonMainWorktreeBranch,
|
||||
forceCurrentBranchMode,
|
||||
@@ -394,7 +415,19 @@ export function AddFeatureDialog({
|
||||
// When a non-main worktree is selected, use its branch name for custom mode
|
||||
setBranchName(selectedNonMainWorktreeBranch || '');
|
||||
setPriority(2);
|
||||
setModelEntry(effectiveDefaultFeatureModel);
|
||||
// Apply defaultThinkingLevel to the model entry (same logic as dialog open)
|
||||
const resetModelId =
|
||||
typeof effectiveDefaultFeatureModel.model === 'string'
|
||||
? effectiveDefaultFeatureModel.model
|
||||
: '';
|
||||
const resetAvailableLevels = getThinkingLevelsForModel(resetModelId);
|
||||
const resetThinkingLevel = resetAvailableLevels.includes(defaultThinkingLevel)
|
||||
? defaultThinkingLevel
|
||||
: resetAvailableLevels[0];
|
||||
setModelEntry({
|
||||
...effectiveDefaultFeatureModel,
|
||||
thinkingLevel: resetThinkingLevel,
|
||||
});
|
||||
setWorkMode(
|
||||
getDefaultWorkMode(useWorktrees, selectedNonMainWorktreeBranch, forceCurrentBranchMode)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { GitPullRequest } from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface WorktreeInfo {
|
||||
path: string;
|
||||
branch: string;
|
||||
isMain: boolean;
|
||||
pr?: {
|
||||
number: number;
|
||||
url: string;
|
||||
title: string;
|
||||
state: string;
|
||||
createdAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ChangePRNumberDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
worktree: WorktreeInfo | null;
|
||||
projectPath: string | null;
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
export function ChangePRNumberDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
worktree,
|
||||
projectPath,
|
||||
onChanged,
|
||||
}: ChangePRNumberDialogProps) {
|
||||
const [prNumberInput, setPrNumberInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Initialize with current PR number when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && worktree?.pr?.number) {
|
||||
setPrNumberInput(String(worktree.pr.number));
|
||||
} else if (open) {
|
||||
setPrNumberInput('');
|
||||
}
|
||||
setError(null);
|
||||
}, [open, worktree]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!worktree) return;
|
||||
|
||||
const trimmed = prNumberInput.trim();
|
||||
if (!/^\d+$/.test(trimmed)) {
|
||||
setError('Please enter a valid positive PR number');
|
||||
return;
|
||||
}
|
||||
const prNumber = Number(trimmed);
|
||||
if (prNumber <= 0) {
|
||||
setError('Please enter a valid positive PR number');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.updatePRNumber) {
|
||||
setError('Worktree API not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.worktree.updatePRNumber(
|
||||
worktree.path,
|
||||
prNumber,
|
||||
projectPath || undefined
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const prInfo = result.result?.prInfo;
|
||||
toast.success('PR tracking updated', {
|
||||
description: prInfo?.title
|
||||
? `Now tracking PR #${prNumber}: ${prInfo.title}`
|
||||
: `Now tracking PR #${prNumber}`,
|
||||
});
|
||||
onOpenChange(false);
|
||||
onChanged();
|
||||
} else {
|
||||
setError(result.error || 'Failed to update PR number');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update PR number');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [worktree, prNumberInput, projectPath, onOpenChange, onChanged]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !isLoading) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
},
|
||||
[isLoading, handleSubmit]
|
||||
);
|
||||
|
||||
if (!worktree) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isLoading) {
|
||||
onOpenChange(isOpen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[400px]" onKeyDown={handleKeyDown}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<GitPullRequest className="w-5 h-5" />
|
||||
Change Tracked PR Number
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update which pull request number is tracked for{' '}
|
||||
<code className="font-mono bg-muted px-1 rounded">{worktree.branch}</code>.
|
||||
{worktree.pr && (
|
||||
<span className="block mt-1 text-xs">
|
||||
Currently tracking PR #{worktree.pr.number}
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-2 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pr-number">Pull Request Number</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm">#</span>
|
||||
<Input
|
||||
id="pr-number"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="e.g. 42"
|
||||
value={prNumberInput}
|
||||
onChange={(e) => {
|
||||
setPrNumberInput(e.target.value);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter the GitHub PR number to associate with this worktree. The PR info will be
|
||||
fetched from GitHub if available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isLoading || !prNumberInput.trim()}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner size="xs" className="mr-2" />
|
||||
Updating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitPullRequest className="w-4 h-4 mr-2" />
|
||||
Update PR
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import { useWorktreeBranches } from '@/hooks/queries';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
|
||||
interface RemoteInfo {
|
||||
name: string;
|
||||
@@ -313,7 +314,7 @@ export function CreatePRDialog({
|
||||
const result = await api.worktree.generatePRDescription(
|
||||
worktree.path,
|
||||
branchNameForApi,
|
||||
prDescriptionModelOverride.effectiveModel,
|
||||
resolveModelString(prDescriptionModelOverride.effectiveModel),
|
||||
prDescriptionModelOverride.effectiveModelEntry.thinkingLevel,
|
||||
prDescriptionModelOverride.effectiveModelEntry.providerId
|
||||
);
|
||||
@@ -501,7 +502,7 @@ export function CreatePRDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[550px] flex flex-col">
|
||||
<DialogContent className="sm:max-w-[550px] max-h-[85vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<GitPullRequest className="w-5 h-5" />
|
||||
|
||||
@@ -25,6 +25,7 @@ export { ViewStashesDialog } from './view-stashes-dialog';
|
||||
export { StashApplyConflictDialog } from './stash-apply-conflict-dialog';
|
||||
export { CherryPickDialog } from './cherry-pick-dialog';
|
||||
export { GitPullDialog } from './git-pull-dialog';
|
||||
export { ChangePRNumberDialog } from './change-pr-number-dialog';
|
||||
export {
|
||||
BranchConflictDialog,
|
||||
type BranchConflictData,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck - feature update logic with partial updates and image/file handling
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
@@ -18,11 +19,29 @@ import { useVerifyFeature, useResumeFeature } from '@/hooks/mutations';
|
||||
import { truncateDescription } from '@/lib/utils';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
|
||||
const logger = createLogger('BoardActions');
|
||||
|
||||
const MAX_DUPLICATES = 50;
|
||||
|
||||
/**
|
||||
* Removes a running task from all worktrees for a given project.
|
||||
* Used when stopping features to ensure the task is removed from all worktree contexts,
|
||||
* not just the current one.
|
||||
*/
|
||||
function removeRunningTaskFromAllWorktrees(projectId: string, featureId: string): void {
|
||||
const store = useAppStore.getState();
|
||||
const prefix = `${projectId}::`;
|
||||
for (const [key, worktreeState] of Object.entries(store.autoModeByWorktree)) {
|
||||
if (key.startsWith(prefix) && worktreeState.runningTasks?.includes(featureId)) {
|
||||
const branchPart = key.slice(prefix.length);
|
||||
const branch = branchPart === '__main__' ? null : branchPart;
|
||||
store.removeRunningTask(projectId, branch, featureId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface UseBoardActionsProps {
|
||||
currentProject: { path: string; id: string } | null;
|
||||
features: Feature[];
|
||||
@@ -84,6 +103,8 @@ export function useBoardActions({
|
||||
onWorktreeAutoSelect,
|
||||
currentWorktreeBranch,
|
||||
}: UseBoardActionsProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// IMPORTANT: Use individual selectors instead of bare useAppStore() to prevent
|
||||
// subscribing to the entire store. Bare useAppStore() causes the host component
|
||||
// (BoardView) to re-render on EVERY store change, which cascades through effects
|
||||
@@ -503,6 +524,10 @@ export function useBoardActions({
|
||||
if (isRunning) {
|
||||
try {
|
||||
await autoMode.stopFeature(featureId);
|
||||
// Remove from all worktrees
|
||||
if (currentProject) {
|
||||
removeRunningTaskFromAllWorktrees(currentProject.id, featureId);
|
||||
}
|
||||
toast.success('Agent stopped', {
|
||||
description: `Stopped and deleted: ${truncateDescription(feature.description)}`,
|
||||
});
|
||||
@@ -533,7 +558,7 @@ export function useBoardActions({
|
||||
removeFeature(featureId);
|
||||
await persistFeatureDelete(featureId);
|
||||
},
|
||||
[features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete]
|
||||
[features, runningAutoTasks, autoMode, removeFeature, persistFeatureDelete, currentProject]
|
||||
);
|
||||
|
||||
const handleRunFeature = useCallback(
|
||||
@@ -999,6 +1024,31 @@ export function useBoardActions({
|
||||
? 'waiting_approval'
|
||||
: 'backlog';
|
||||
|
||||
// Remove the running task from ALL worktrees for this project.
|
||||
// autoMode.stopFeature only removes from its scoped worktree (branchName),
|
||||
// but the feature may be tracked under a different worktree branch.
|
||||
// Without this, runningAutoTasksAllWorktrees still contains the feature
|
||||
// and the board column logic forces it into in_progress.
|
||||
if (currentProject) {
|
||||
removeRunningTaskFromAllWorktrees(currentProject.id, feature.id);
|
||||
}
|
||||
|
||||
// Optimistically update the React Query features cache so the board
|
||||
// moves the card immediately. Without this, the card stays in
|
||||
// "in_progress" until the next poll cycle (30s) because the async
|
||||
// refetch races with the persistFeatureUpdate write.
|
||||
if (currentProject) {
|
||||
queryClient.setQueryData(
|
||||
queryKeys.features.all(currentProject.path),
|
||||
(oldFeatures: Feature[] | undefined) => {
|
||||
if (!oldFeatures) return oldFeatures;
|
||||
return oldFeatures.map((f) =>
|
||||
f.id === feature.id ? { ...f, status: targetStatus } : f
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (targetStatus !== feature.status) {
|
||||
moveFeature(feature.id, targetStatus);
|
||||
// Must await to ensure file is written before user can restart
|
||||
@@ -1020,7 +1070,7 @@ export function useBoardActions({
|
||||
});
|
||||
}
|
||||
},
|
||||
[autoMode, moveFeature, persistFeatureUpdate]
|
||||
[autoMode, moveFeature, persistFeatureUpdate, currentProject, queryClient]
|
||||
);
|
||||
|
||||
const handleStartNextFeatures = useCallback(async () => {
|
||||
@@ -1137,6 +1187,12 @@ export function useBoardActions({
|
||||
})
|
||||
)
|
||||
);
|
||||
// Remove from all worktrees
|
||||
if (currentProject) {
|
||||
for (const feature of runningVerified) {
|
||||
removeRunningTaskFromAllWorktrees(currentProject.id, feature.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use bulk update API for a single server request instead of N individual calls
|
||||
|
||||
@@ -6,13 +6,21 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Sparkles, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import { EnhancementMode, ENHANCEMENT_MODE_LABELS } from './enhancement-constants';
|
||||
import {
|
||||
EnhancementMode,
|
||||
ENHANCEMENT_MODE_LABELS,
|
||||
REWRITE_MODES,
|
||||
ADDITIVE_MODES,
|
||||
isAdditiveMode,
|
||||
} from './enhancement-constants';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
|
||||
const logger = createLogger('EnhanceWithAI');
|
||||
@@ -79,7 +87,10 @@ export function EnhanceWithAI({
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
const originalText = value;
|
||||
const enhancedText = result.enhancedText;
|
||||
// For additive modes, prepend the original description above the AI-generated content
|
||||
const enhancedText = isAdditiveMode(enhancementMode)
|
||||
? `${originalText.trim()}\n\n${result.enhancedText.trim()}`
|
||||
: result.enhancedText;
|
||||
onChange(enhancedText);
|
||||
|
||||
// Track in history if callback provided (includes original for restoration)
|
||||
@@ -119,13 +130,19 @@ export function EnhanceWithAI({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{(Object.entries(ENHANCEMENT_MODE_LABELS) as [EnhancementMode, string][]).map(
|
||||
([mode, label]) => (
|
||||
<DropdownMenuItem key={mode} onClick={() => setEnhancementMode(mode)}>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
<DropdownMenuLabel>Rewrite</DropdownMenuLabel>
|
||||
{REWRITE_MODES.map((mode) => (
|
||||
<DropdownMenuItem key={mode} onClick={() => setEnhancementMode(mode)}>
|
||||
{ENHANCEMENT_MODE_LABELS[mode]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Append Details</DropdownMenuLabel>
|
||||
{ADDITIVE_MODES.map((mode) => (
|
||||
<DropdownMenuItem key={mode} onClick={() => setEnhancementMode(mode)}>
|
||||
{ENHANCEMENT_MODE_LABELS[mode]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Enhancement mode options for AI-powered prompt improvement */
|
||||
export type EnhancementMode = 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
|
||||
import type { EnhancementMode } from '@automaker/types';
|
||||
export type { EnhancementMode } from '@automaker/types';
|
||||
|
||||
/** Labels for enhancement modes displayed in the UI */
|
||||
export const ENHANCEMENT_MODE_LABELS: Record<EnhancementMode, string> = {
|
||||
@@ -18,3 +18,14 @@ export const ENHANCEMENT_MODE_DESCRIPTIONS: Record<EnhancementMode, string> = {
|
||||
acceptance: 'Add specific acceptance criteria and test cases',
|
||||
'ux-reviewer': 'Add user experience considerations and flows',
|
||||
};
|
||||
|
||||
/** Modes that rewrite/replace the entire description */
|
||||
export const REWRITE_MODES: EnhancementMode[] = ['improve', 'simplify'];
|
||||
|
||||
/** Modes that append additional content below the original description */
|
||||
export const ADDITIVE_MODES: EnhancementMode[] = ['technical', 'acceptance', 'ux-reviewer'];
|
||||
|
||||
/** Check if a mode appends content rather than replacing */
|
||||
export function isAdditiveMode(mode: EnhancementMode): boolean {
|
||||
return ADDITIVE_MODES.includes(mode);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ import {
|
||||
XCircle,
|
||||
CheckCircle,
|
||||
Settings2,
|
||||
ArrowLeftRight,
|
||||
Check,
|
||||
Hash,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -105,6 +108,7 @@ interface WorktreeActionsDropdownProps {
|
||||
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onChangePRNumber?: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onAutoAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onResolveConflicts: (worktree: WorktreeInfo) => void;
|
||||
@@ -149,6 +153,14 @@ interface WorktreeActionsDropdownProps {
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** List of remote names that have a branch matching the current branch name */
|
||||
remotesWithBranch?: string[];
|
||||
/** Available worktrees for swapping into this slot (non-main only) */
|
||||
availableWorktreesForSwap?: WorktreeInfo[];
|
||||
/** The slot index for this tab in the pinned list (0-based, excluding main) */
|
||||
slotIndex?: number;
|
||||
/** Callback when user swaps this slot to a different worktree */
|
||||
onSwapWorktree?: (slotIndex: number, newBranch: string) => void;
|
||||
/** List of currently pinned branch names (to show which are pinned in the swap dropdown) */
|
||||
pinnedBranches?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +271,7 @@ export function WorktreeActionsDropdown({
|
||||
onDiscardChanges,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onChangePRNumber,
|
||||
onAddressPRComments,
|
||||
onAutoAddressPRComments,
|
||||
onResolveConflicts,
|
||||
@@ -287,6 +300,10 @@ export function WorktreeActionsDropdown({
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
remotesWithBranch,
|
||||
availableWorktreesForSwap,
|
||||
slotIndex,
|
||||
onSwapWorktree,
|
||||
pinnedBranches,
|
||||
}: WorktreeActionsDropdownProps) {
|
||||
// Get available editors for the "Open In" submenu
|
||||
const { editors } = useAvailableEditors();
|
||||
@@ -1334,6 +1351,12 @@ export function WorktreeActionsDropdown({
|
||||
<Zap className="w-3.5 h-3.5 mr-2" />
|
||||
Address PR Comments
|
||||
</DropdownMenuItem>
|
||||
{onChangePRNumber && (
|
||||
<DropdownMenuItem onClick={() => onChangePRNumber(worktree)} className="text-xs">
|
||||
<Hash className="w-3.5 h-3.5 mr-2" />
|
||||
Change PR Number
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
@@ -1359,6 +1382,36 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{/* Swap Worktree submenu - only shown for non-main slots when there are other worktrees to swap to */}
|
||||
{!worktree.isMain &&
|
||||
availableWorktreesForSwap &&
|
||||
availableWorktreesForSwap.length > 1 &&
|
||||
slotIndex !== undefined &&
|
||||
onSwapWorktree && (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="text-xs">
|
||||
<ArrowLeftRight className="w-3.5 h-3.5 mr-2" />
|
||||
Swap Worktree
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-64 max-h-80 overflow-y-auto">
|
||||
{availableWorktreesForSwap
|
||||
.filter((wt) => wt.branch !== worktree.branch)
|
||||
.map((wt) => {
|
||||
const isPinned = pinnedBranches?.includes(wt.branch);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={wt.path}
|
||||
onSelect={() => onSwapWorktree(slotIndex, wt.branch)}
|
||||
className="flex items-center gap-2 cursor-pointer font-mono text-xs"
|
||||
>
|
||||
<span className="truncate flex-1">{wt.branch}</span>
|
||||
{isPinned && <Check className="w-3 h-3 shrink-0 text-muted-foreground" />}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)}
|
||||
{!worktree.isMain && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDeleteWorktree(worktree)}
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface WorktreeDropdownProps {
|
||||
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onChangePRNumber?: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onAutoAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onResolveConflicts: (worktree: WorktreeInfo) => void;
|
||||
@@ -148,6 +149,8 @@ export interface WorktreeDropdownProps {
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** List of remote names that have a branch matching the current branch name */
|
||||
remotesWithBranch?: string[];
|
||||
/** When false, the trigger button uses a subdued style instead of the primary highlight. Defaults to true. */
|
||||
highlightTrigger?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +218,7 @@ export function WorktreeDropdown({
|
||||
onDiscardChanges,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onChangePRNumber,
|
||||
onAddressPRComments,
|
||||
onAutoAddressPRComments,
|
||||
onResolveConflicts,
|
||||
@@ -245,10 +249,13 @@ export function WorktreeDropdown({
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
remotesWithBranch,
|
||||
highlightTrigger = true,
|
||||
}: WorktreeDropdownProps) {
|
||||
// Find the currently selected worktree to display in the trigger
|
||||
const selectedWorktree = worktrees.find((w) => isWorktreeSelected(w));
|
||||
const displayBranch = selectedWorktree?.branch || 'Select worktree';
|
||||
const displayBranch =
|
||||
selectedWorktree?.branch ??
|
||||
(worktrees.length > 0 ? `+${worktrees.length} more` : 'Select worktree');
|
||||
const { truncated: truncatedBranch, isTruncated: isBranchNameTruncated } = truncateBranchName(
|
||||
displayBranch,
|
||||
MAX_TRIGGER_BRANCH_NAME_LENGTH
|
||||
@@ -292,15 +299,28 @@ export function WorktreeDropdown({
|
||||
const triggerButton = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={selectedWorktree && highlightTrigger ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn(
|
||||
'h-7 px-3 gap-1.5 font-mono text-xs bg-secondary/50 hover:bg-secondary min-w-0 border-r-0 rounded-r-none'
|
||||
'h-7 px-3 gap-1.5 font-mono text-xs min-w-0',
|
||||
selectedWorktree &&
|
||||
highlightTrigger &&
|
||||
'bg-primary text-primary-foreground border-r-0 rounded-l-md rounded-r-none',
|
||||
selectedWorktree &&
|
||||
!highlightTrigger &&
|
||||
'bg-secondary/50 hover:bg-secondary border-r-0 rounded-l-md rounded-r-none',
|
||||
!selectedWorktree && 'bg-secondary/50 hover:bg-secondary rounded-md'
|
||||
)}
|
||||
disabled={isActivating}
|
||||
>
|
||||
{/* Running/Activating indicator */}
|
||||
{(selectedStatus.isRunning || isActivating) && <Spinner size="xs" className="shrink-0" />}
|
||||
{(selectedStatus.isRunning || isActivating) && (
|
||||
<Spinner
|
||||
size="xs"
|
||||
className="shrink-0"
|
||||
variant={selectedWorktree && highlightTrigger ? 'foreground' : 'primary'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Branch icon */}
|
||||
<GitBranch className="w-3.5 h-3.5 shrink-0" />
|
||||
@@ -403,7 +423,14 @@ export function WorktreeDropdown({
|
||||
<ChevronDown className="w-3 h-3 shrink-0 ml-auto" />
|
||||
</Button>
|
||||
),
|
||||
[isActivating, selectedStatus, truncatedBranch, selectedWorktree, branchCardCounts]
|
||||
[
|
||||
isActivating,
|
||||
selectedStatus,
|
||||
truncatedBranch,
|
||||
selectedWorktree,
|
||||
branchCardCounts,
|
||||
highlightTrigger,
|
||||
]
|
||||
);
|
||||
|
||||
// Wrap trigger button with dropdown trigger first to ensure ref is passed correctly
|
||||
@@ -490,7 +517,7 @@ export function WorktreeDropdown({
|
||||
{selectedWorktree?.isMain && (
|
||||
<BranchSwitchDropdown
|
||||
worktree={selectedWorktree}
|
||||
isSelected={true}
|
||||
isSelected={highlightTrigger}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
@@ -507,7 +534,7 @@ export function WorktreeDropdown({
|
||||
{selectedWorktree && (
|
||||
<WorktreeActionsDropdown
|
||||
worktree={selectedWorktree}
|
||||
isSelected={true}
|
||||
isSelected={highlightTrigger}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
@@ -541,6 +568,7 @@ export function WorktreeDropdown({
|
||||
onDiscardChanges={onDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
|
||||
@@ -66,6 +66,7 @@ interface WorktreeTabProps {
|
||||
onDiscardChanges: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onChangePRNumber?: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onAutoAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onResolveConflicts: (worktree: WorktreeInfo) => void;
|
||||
@@ -118,6 +119,14 @@ interface WorktreeTabProps {
|
||||
onSetTracking?: (worktree: WorktreeInfo, remote: string) => void;
|
||||
/** List of remote names that have a branch matching the current branch name */
|
||||
remotesWithBranch?: string[];
|
||||
/** Available worktrees for swapping into this slot (non-main only) */
|
||||
availableWorktreesForSwap?: WorktreeInfo[];
|
||||
/** The slot index for this tab in the pinned list (0-based, excluding main) */
|
||||
slotIndex?: number;
|
||||
/** Callback when user swaps this slot to a different worktree */
|
||||
onSwapWorktree?: (slotIndex: number, newBranch: string) => void;
|
||||
/** List of currently pinned branch names (to show which are pinned in the swap dropdown) */
|
||||
pinnedBranches?: string[];
|
||||
}
|
||||
|
||||
export function WorktreeTab({
|
||||
@@ -164,6 +173,7 @@ export function WorktreeTab({
|
||||
onDiscardChanges,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onChangePRNumber,
|
||||
onAddressPRComments,
|
||||
onAutoAddressPRComments,
|
||||
onResolveConflicts,
|
||||
@@ -196,6 +206,10 @@ export function WorktreeTab({
|
||||
onSyncWithRemote,
|
||||
onSetTracking,
|
||||
remotesWithBranch,
|
||||
availableWorktreesForSwap,
|
||||
slotIndex,
|
||||
onSwapWorktree,
|
||||
pinnedBranches,
|
||||
}: WorktreeTabProps) {
|
||||
// Make the worktree tab a drop target for feature cards
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
@@ -542,6 +556,7 @@ export function WorktreeTab({
|
||||
onDiscardChanges={onDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
@@ -570,6 +585,10 @@ export function WorktreeTab({
|
||||
onSyncWithRemote={onSyncWithRemote}
|
||||
onSetTracking={onSetTracking}
|
||||
remotesWithBranch={remotesWithBranch}
|
||||
availableWorktreesForSwap={availableWorktreesForSwap}
|
||||
slotIndex={slotIndex}
|
||||
onSwapWorktree={onSwapWorktree}
|
||||
pinnedBranches={pinnedBranches}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -120,6 +120,7 @@ export interface WorktreePanelProps {
|
||||
onDeleteWorktree: (worktree: WorktreeInfo) => void;
|
||||
onCommit: (worktree: WorktreeInfo) => void;
|
||||
onCreatePR: (worktree: WorktreeInfo) => void;
|
||||
onChangePRNumber?: (worktree: WorktreeInfo) => void;
|
||||
onCreateBranch: (worktree: WorktreeInfo) => void;
|
||||
onAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
onAutoAddressPRComments: (worktree: WorktreeInfo, prInfo: PRInfo) => void;
|
||||
|
||||
@@ -26,11 +26,11 @@ import {
|
||||
} from './hooks';
|
||||
import {
|
||||
WorktreeTab,
|
||||
WorktreeDropdown,
|
||||
DevServerLogsPanel,
|
||||
WorktreeMobileDropdown,
|
||||
WorktreeActionsDropdown,
|
||||
BranchSwitchDropdown,
|
||||
WorktreeDropdown,
|
||||
} from './components';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import {
|
||||
@@ -50,8 +50,9 @@ import type { SelectRemoteOperation } from '../dialogs';
|
||||
import { TestLogsPanel } from '@/components/ui/test-logs-panel';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
|
||||
/** Threshold for switching from tabs to dropdown layout (number of worktrees) */
|
||||
const WORKTREE_DROPDOWN_THRESHOLD = 3;
|
||||
// Stable empty array to avoid creating a new [] reference on every render
|
||||
// when pinnedWorktreeBranchesByProject[projectPath] is undefined
|
||||
const EMPTY_BRANCHES: string[] = [];
|
||||
|
||||
export function WorktreePanel({
|
||||
projectPath,
|
||||
@@ -59,6 +60,7 @@ export function WorktreePanel({
|
||||
onDeleteWorktree,
|
||||
onCommit,
|
||||
onCreatePR,
|
||||
onChangePRNumber,
|
||||
onCreateBranch,
|
||||
onAddressPRComments,
|
||||
onAutoAddressPRComments,
|
||||
@@ -99,7 +101,6 @@ export function WorktreePanel({
|
||||
aheadCount,
|
||||
behindCount,
|
||||
hasRemoteBranch,
|
||||
trackingRemote,
|
||||
getTrackingRemote,
|
||||
remotesWithBranch,
|
||||
isLoadingBranches,
|
||||
@@ -139,13 +140,107 @@ export function WorktreePanel({
|
||||
features,
|
||||
});
|
||||
|
||||
// Pinned worktrees count from store
|
||||
const pinnedWorktreesCount = useAppStore(
|
||||
(state) => state.pinnedWorktreesCountByProject[projectPath] ?? 0
|
||||
);
|
||||
const pinnedWorktreeBranchesRaw = useAppStore(
|
||||
(state) => state.pinnedWorktreeBranchesByProject[projectPath]
|
||||
);
|
||||
const pinnedWorktreeBranches = pinnedWorktreeBranchesRaw ?? EMPTY_BRANCHES;
|
||||
const setPinnedWorktreeBranches = useAppStore((state) => state.setPinnedWorktreeBranches);
|
||||
const swapPinnedWorktreeBranch = useAppStore((state) => state.swapPinnedWorktreeBranch);
|
||||
|
||||
// Resolve pinned worktrees from explicit branch assignments
|
||||
// Shows exactly pinnedWorktreesCount slots, each with a specific worktree.
|
||||
// Main worktree is always slot 0. Other slots can be swapped by the user.
|
||||
const pinnedWorktrees = useMemo(() => {
|
||||
const mainWt = worktrees.find((w) => w.isMain);
|
||||
const otherWts = worktrees.filter((w) => !w.isMain);
|
||||
|
||||
// Slot 0 is always main worktree
|
||||
const result: WorktreeInfo[] = mainWt ? [mainWt] : [];
|
||||
|
||||
// pinnedWorktreesCount represents only non-main worktrees; main is always shown separately
|
||||
const otherSlotCount = Math.max(0, pinnedWorktreesCount);
|
||||
|
||||
if (otherSlotCount > 0 && otherWts.length > 0) {
|
||||
// Use explicit branch assignments if available
|
||||
const assignedBranches = pinnedWorktreeBranches;
|
||||
const usedBranches = new Set<string>();
|
||||
|
||||
for (let i = 0; i < otherSlotCount; i++) {
|
||||
const assignedBranch = assignedBranches[i];
|
||||
let wt: WorktreeInfo | undefined;
|
||||
|
||||
// Try to find the explicitly assigned worktree
|
||||
if (assignedBranch) {
|
||||
wt = otherWts.find((w) => w.branch === assignedBranch && !usedBranches.has(w.branch));
|
||||
}
|
||||
|
||||
// Fall back to next available worktree if assigned one doesn't exist
|
||||
if (!wt) {
|
||||
wt = otherWts.find((w) => !usedBranches.has(w.branch));
|
||||
}
|
||||
|
||||
if (wt) {
|
||||
result.push(wt);
|
||||
usedBranches.add(wt.branch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [worktrees, pinnedWorktreesCount, pinnedWorktreeBranches]);
|
||||
|
||||
// All non-main worktrees available for swapping into slots
|
||||
const availableWorktreesForSwap = useMemo(() => {
|
||||
return worktrees.filter((w) => !w.isMain);
|
||||
}, [worktrees]);
|
||||
|
||||
// Handle swapping a worktree in a specific slot
|
||||
const handleSwapWorktreeSlot = useCallback(
|
||||
(slotIndex: number, newBranch: string) => {
|
||||
swapPinnedWorktreeBranch(projectPath, slotIndex, newBranch);
|
||||
},
|
||||
[projectPath, swapPinnedWorktreeBranch]
|
||||
);
|
||||
|
||||
// Initialize pinned branch assignments when worktrees change
|
||||
// This ensures new worktrees get default slot assignments
|
||||
// Read store state directly inside the effect to avoid a dependency cycle
|
||||
// (the effect writes to the same state it would otherwise depend on)
|
||||
useEffect(() => {
|
||||
const mainWt = worktrees.find((w) => w.isMain);
|
||||
const otherWts = worktrees.filter((w) => !w.isMain);
|
||||
const otherSlotCount = Math.max(0, pinnedWorktreesCount);
|
||||
|
||||
const storedBranches = useAppStore.getState().pinnedWorktreeBranchesByProject[projectPath];
|
||||
if (otherSlotCount > 0 && otherWts.length > 0) {
|
||||
const existing = storedBranches ?? [];
|
||||
if (existing.length < otherSlotCount) {
|
||||
const used = new Set(existing.filter(Boolean));
|
||||
const filled = [...existing];
|
||||
for (const wt of otherWts) {
|
||||
if (filled.length >= otherSlotCount) break;
|
||||
if (!used.has(wt.branch)) {
|
||||
filled.push(wt.branch);
|
||||
used.add(wt.branch);
|
||||
}
|
||||
}
|
||||
if (filled.length > 0) {
|
||||
setPinnedWorktreeBranches(projectPath, filled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [worktrees, pinnedWorktreesCount, projectPath, setPinnedWorktreeBranches]);
|
||||
|
||||
// Auto-mode state management using the store
|
||||
// Use separate selectors to avoid creating new object references on each render
|
||||
const autoModeByWorktree = useAppStore((state) => state.autoModeByWorktree);
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
const setAutoModeRunning = useAppStore((state) => state.setAutoModeRunning);
|
||||
const getMaxConcurrencyForWorktree = useAppStore((state) => state.getMaxConcurrencyForWorktree);
|
||||
|
||||
// Helper to generate worktree key for auto-mode (inlined to avoid selector issues)
|
||||
const getAutoModeWorktreeKey = useCallback(
|
||||
(projectId: string, branchName: string | null): string => {
|
||||
@@ -651,18 +746,6 @@ export function WorktreePanel({
|
||||
// Keep logPanelWorktree set for smooth close animation
|
||||
}, []);
|
||||
|
||||
// Wrap handleStartDevServer to auto-open the logs panel so the user
|
||||
// can see output immediately (including failure reasons)
|
||||
const handleStartDevServerAndShowLogs = useCallback(
|
||||
async (worktree: WorktreeInfo) => {
|
||||
// Open logs panel immediately so output is visible from the start
|
||||
setLogPanelWorktree(worktree);
|
||||
setLogPanelOpen(true);
|
||||
await handleStartDevServer(worktree);
|
||||
},
|
||||
[handleStartDevServer]
|
||||
);
|
||||
|
||||
// Handle opening the push to remote dialog
|
||||
const handlePushNewBranch = useCallback((worktree: WorktreeInfo) => {
|
||||
setPushToRemoteWorktree(worktree);
|
||||
@@ -887,7 +970,6 @@ export function WorktreePanel({
|
||||
);
|
||||
|
||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||
const nonMainWorktrees = worktrees.filter((w) => !w.isMain);
|
||||
|
||||
// Mobile view: single dropdown for all worktrees
|
||||
if (isMobile) {
|
||||
@@ -965,12 +1047,13 @@ export function WorktreePanel({
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
@@ -1145,56 +1228,124 @@ export function WorktreePanel({
|
||||
);
|
||||
}
|
||||
|
||||
// Use dropdown layout when worktree count meets or exceeds the threshold
|
||||
const useDropdownLayout = worktrees.length >= WORKTREE_DROPDOWN_THRESHOLD;
|
||||
// Desktop view: pinned worktrees as individual tabs (each slot can be swapped)
|
||||
|
||||
// Desktop view: full tabs layout or dropdown layout depending on worktree count
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground mr-2">
|
||||
{useDropdownLayout ? 'Worktree:' : 'Branch:'}
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b border-border bg-glass/50 backdrop-blur-sm">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm text-muted-foreground mr-2 shrink-0">Worktree:</span>
|
||||
|
||||
{/* Dropdown layout for 3+ worktrees */}
|
||||
{useDropdownLayout ? (
|
||||
<>
|
||||
<WorktreeDropdown
|
||||
worktrees={worktrees}
|
||||
isWorktreeSelected={isWorktreeSelected}
|
||||
hasRunningFeatures={hasRunningFeatures}
|
||||
{/* When only 1 pinned slot (main only) and there are other worktrees,
|
||||
use a compact dropdown to switch between them without highlighting main */}
|
||||
{pinnedWorktreesCount === 0 && availableWorktreesForSwap.length > 0 ? (
|
||||
<WorktreeDropdown
|
||||
worktrees={worktrees}
|
||||
isWorktreeSelected={isWorktreeSelected}
|
||||
hasRunningFeatures={hasRunningFeatures}
|
||||
isActivating={isActivating}
|
||||
branchCardCounts={branchCardCounts}
|
||||
isDevServerRunning={isDevServerRunning}
|
||||
getDevServerInfo={getDevServerInfo}
|
||||
isAutoModeRunningForWorktree={isAutoModeRunningForWorktree}
|
||||
isTestRunningForWorktree={isTestRunningForWorktree}
|
||||
getTestSessionInfo={getTestSessionInfo}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
isLoadingBranches={isLoadingBranches}
|
||||
isSwitching={isSwitching}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
getTrackingRemote={getTrackingRemote}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
hasTestCommand={hasTestCommand}
|
||||
isStartingTests={isStartingTests}
|
||||
hasInitScript={hasInitScript}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange}
|
||||
onPull={handlePullWithRemoteSelection}
|
||||
onPush={handlePushWithRemoteSelection}
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
remotesCache={remotesCache}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onViewCommits={handleViewCommits}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
onRunInitScript={handleRunInitScript}
|
||||
onToggleAutoMode={handleToggleAutoMode}
|
||||
onStartTests={handleStartTests}
|
||||
onStopTests={handleStopTests}
|
||||
onViewTestLogs={handleViewTestLogs}
|
||||
onStashChanges={handleStashChanges}
|
||||
onViewStashes={handleViewStashes}
|
||||
onCherryPick={handleCherryPick}
|
||||
onAbortOperation={handleAbortOperation}
|
||||
onContinueOperation={handleContinueOperation}
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={handleRunTerminalScript}
|
||||
onEditScripts={handleEditScripts}
|
||||
highlightTrigger={false}
|
||||
/>
|
||||
) : pinnedWorktreesCount === 0 ? (
|
||||
/* Only main worktree, no others exist - render main tab without highlight */
|
||||
mainWorktree && (
|
||||
<WorktreeTab
|
||||
worktree={mainWorktree}
|
||||
cardCount={branchCardCounts?.[mainWorktree.branch]}
|
||||
hasChanges={mainWorktree.hasChanges}
|
||||
changedFilesCount={mainWorktree.changedFilesCount}
|
||||
isSelected={false}
|
||||
isRunning={hasRunningFeatures(mainWorktree)}
|
||||
isActivating={isActivating}
|
||||
branchCardCounts={branchCardCounts}
|
||||
isDevServerRunning={isDevServerRunning}
|
||||
getDevServerInfo={getDevServerInfo}
|
||||
isAutoModeRunningForWorktree={isAutoModeRunningForWorktree}
|
||||
isTestRunningForWorktree={isTestRunningForWorktree}
|
||||
getTestSessionInfo={getTestSessionInfo}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
// Branch switching props
|
||||
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
||||
devServerInfo={getDevServerInfo(mainWorktree)}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
isLoadingBranches={isLoadingBranches}
|
||||
isSwitching={isSwitching}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
// Action dropdown props
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
trackingRemote={trackingRemote}
|
||||
getTrackingRemote={getTrackingRemote}
|
||||
trackingRemote={getTrackingRemote(mainWorktree.path)}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
hasTestCommand={hasTestCommand}
|
||||
isAutoModeRunning={isAutoModeRunningForWorktree(mainWorktree)}
|
||||
isStartingTests={isStartingTests}
|
||||
hasInitScript={hasInitScript}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange}
|
||||
isTestRunning={isTestRunningForWorktree(mainWorktree)}
|
||||
testSessionInfo={getTestSessionInfo(mainWorktree)}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(mainWorktree)}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(mainWorktree)}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
onPull={handlePullWithRemoteSelection}
|
||||
onPush={handlePushWithRemoteSelection}
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
@@ -1205,7 +1356,7 @@ export function WorktreePanel({
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotesWithBranch={remotesWithBranch}
|
||||
remotesCache={remotesCache}
|
||||
remotes={remotesCache[mainWorktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
@@ -1214,12 +1365,13 @@ export function WorktreePanel({
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
@@ -1233,247 +1385,138 @@ export function WorktreePanel({
|
||||
onCherryPick={handleCherryPick}
|
||||
onAbortOperation={handleAbortOperation}
|
||||
onContinueOperation={handleContinueOperation}
|
||||
hasInitScript={hasInitScript}
|
||||
hasTestCommand={hasTestCommand}
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={handleRunTerminalScript}
|
||||
onEditScripts={handleEditScripts}
|
||||
/>
|
||||
|
||||
{useWorktreesEnabled && (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={onCreateWorktree}
|
||||
title="Create new worktree"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={async () => {
|
||||
const removedWorktrees = await fetchWorktrees();
|
||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||
onRemovedWorktrees(removedWorktrees);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
title="Refresh worktrees"
|
||||
>
|
||||
{isLoading ? <Spinner size="xs" /> : <RefreshCw className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
/* Standard tabs layout for 1-2 worktrees */
|
||||
/* Multiple pinned slots - show individual tabs */
|
||||
pinnedWorktrees.map((worktree, index) => {
|
||||
const hasOtherWorktrees = worktrees.length > 1;
|
||||
const effectiveIsSelected =
|
||||
isWorktreeSelected(worktree) && (hasOtherWorktrees || !worktree.isMain);
|
||||
|
||||
// Slot index for swap (0-based, excluding main which is always slot 0)
|
||||
const slotIndex = worktree.isMain ? -1 : index - (pinnedWorktrees[0]?.isMain ? 1 : 0);
|
||||
|
||||
return (
|
||||
<WorktreeTab
|
||||
key={worktree.path}
|
||||
worktree={worktree}
|
||||
cardCount={branchCardCounts?.[worktree.branch]}
|
||||
hasChanges={worktree.hasChanges}
|
||||
changedFilesCount={worktree.changedFilesCount}
|
||||
isSelected={effectiveIsSelected}
|
||||
isRunning={hasRunningFeatures(worktree)}
|
||||
isActivating={isActivating}
|
||||
isDevServerRunning={isDevServerRunning(worktree)}
|
||||
devServerInfo={getDevServerInfo(worktree)}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
isLoadingBranches={isLoadingBranches}
|
||||
isSwitching={isSwitching}
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
trackingRemote={getTrackingRemote(worktree.path)}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
isAutoModeRunning={isAutoModeRunningForWorktree(worktree)}
|
||||
isStartingTests={isStartingTests}
|
||||
isTestRunning={isTestRunningForWorktree(worktree)}
|
||||
testSessionInfo={getTestSessionInfo(worktree)}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
onPull={handlePullWithRemoteSelection}
|
||||
onPush={handlePushWithRemoteSelection}
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
remotes={remotesCache[worktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onViewCommits={handleViewCommits}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onChangePRNumber={onChangePRNumber}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServer}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
onRunInitScript={handleRunInitScript}
|
||||
onToggleAutoMode={handleToggleAutoMode}
|
||||
onStartTests={handleStartTests}
|
||||
onStopTests={handleStopTests}
|
||||
onViewTestLogs={handleViewTestLogs}
|
||||
onStashChanges={handleStashChanges}
|
||||
onViewStashes={handleViewStashes}
|
||||
onCherryPick={handleCherryPick}
|
||||
onAbortOperation={handleAbortOperation}
|
||||
onContinueOperation={handleContinueOperation}
|
||||
hasInitScript={hasInitScript}
|
||||
hasTestCommand={hasTestCommand}
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={handleRunTerminalScript}
|
||||
onEditScripts={handleEditScripts}
|
||||
availableWorktreesForSwap={!worktree.isMain ? availableWorktreesForSwap : undefined}
|
||||
slotIndex={slotIndex >= 0 ? slotIndex : undefined}
|
||||
onSwapWorktree={slotIndex >= 0 ? handleSwapWorktreeSlot : undefined}
|
||||
pinnedBranches={pinnedWorktrees.map((w) => w.branch)}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Create and refresh buttons */}
|
||||
{useWorktreesEnabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
{mainWorktree && (
|
||||
<WorktreeTab
|
||||
key={mainWorktree.path}
|
||||
worktree={mainWorktree}
|
||||
cardCount={branchCardCounts?.[mainWorktree.branch]}
|
||||
hasChanges={mainWorktree.hasChanges}
|
||||
changedFilesCount={mainWorktree.changedFilesCount}
|
||||
isSelected={isWorktreeSelected(mainWorktree)}
|
||||
isRunning={hasRunningFeatures(mainWorktree)}
|
||||
isActivating={isActivating}
|
||||
isDevServerRunning={isDevServerRunning(mainWorktree)}
|
||||
devServerInfo={getDevServerInfo(mainWorktree)}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
isLoadingBranches={isLoadingBranches}
|
||||
isSwitching={isSwitching}
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
trackingRemote={getTrackingRemote(mainWorktree.path)}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
isAutoModeRunning={isAutoModeRunningForWorktree(mainWorktree)}
|
||||
isStartingTests={isStartingTests}
|
||||
isTestRunning={isTestRunningForWorktree(mainWorktree)}
|
||||
testSessionInfo={getTestSessionInfo(mainWorktree)}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(mainWorktree)}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(mainWorktree)}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
onPull={handlePullWithRemoteSelection}
|
||||
onPush={handlePushWithRemoteSelection}
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotes={remotesCache[mainWorktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onViewCommits={handleViewCommits}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
onRunInitScript={handleRunInitScript}
|
||||
onToggleAutoMode={handleToggleAutoMode}
|
||||
onStartTests={handleStartTests}
|
||||
onStopTests={handleStopTests}
|
||||
onViewTestLogs={handleViewTestLogs}
|
||||
onStashChanges={handleStashChanges}
|
||||
onViewStashes={handleViewStashes}
|
||||
onCherryPick={handleCherryPick}
|
||||
onAbortOperation={handleAbortOperation}
|
||||
onContinueOperation={handleContinueOperation}
|
||||
hasInitScript={hasInitScript}
|
||||
hasTestCommand={hasTestCommand}
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={handleRunTerminalScript}
|
||||
onEditScripts={handleEditScripts}
|
||||
remotesWithBranch={remotesWithBranch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={onCreateWorktree}
|
||||
title="Create new worktree"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{/* Worktrees section - only show if enabled and not using dropdown layout */}
|
||||
{useWorktreesEnabled && (
|
||||
<>
|
||||
<div className="w-px h-5 bg-border mx-2" />
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground mr-2">Worktrees:</span>
|
||||
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{nonMainWorktrees.map((worktree) => {
|
||||
const cardCount = branchCardCounts?.[worktree.branch];
|
||||
return (
|
||||
<WorktreeTab
|
||||
key={worktree.path}
|
||||
worktree={worktree}
|
||||
cardCount={cardCount}
|
||||
hasChanges={worktree.hasChanges}
|
||||
changedFilesCount={worktree.changedFilesCount}
|
||||
isSelected={isWorktreeSelected(worktree)}
|
||||
isRunning={hasRunningFeatures(worktree)}
|
||||
isActivating={isActivating}
|
||||
isDevServerRunning={isDevServerRunning(worktree)}
|
||||
devServerInfo={getDevServerInfo(worktree)}
|
||||
branches={branches}
|
||||
filteredBranches={filteredBranches}
|
||||
branchFilter={branchFilter}
|
||||
isLoadingBranches={isLoadingBranches}
|
||||
isSwitching={isSwitching}
|
||||
isPulling={isPulling}
|
||||
isPushing={isPushing}
|
||||
isStartingDevServer={isStartingDevServer}
|
||||
aheadCount={aheadCount}
|
||||
behindCount={behindCount}
|
||||
hasRemoteBranch={hasRemoteBranch}
|
||||
trackingRemote={getTrackingRemote(worktree.path)}
|
||||
gitRepoStatus={gitRepoStatus}
|
||||
isAutoModeRunning={isAutoModeRunningForWorktree(worktree)}
|
||||
isStartingTests={isStartingTests}
|
||||
isTestRunning={isTestRunningForWorktree(worktree)}
|
||||
testSessionInfo={getTestSessionInfo(worktree)}
|
||||
onSelectWorktree={handleSelectWorktree}
|
||||
onBranchDropdownOpenChange={handleBranchDropdownOpenChange(worktree)}
|
||||
onActionsDropdownOpenChange={handleActionsDropdownOpenChange(worktree)}
|
||||
onBranchFilterChange={setBranchFilter}
|
||||
onSwitchBranch={handleSwitchBranch}
|
||||
onCreateBranch={onCreateBranch}
|
||||
onPull={handlePullWithRemoteSelection}
|
||||
onPush={handlePushWithRemoteSelection}
|
||||
onPushNewBranch={handlePushNewBranch}
|
||||
onPullWithRemote={handlePullWithSpecificRemote}
|
||||
onPushWithRemote={handlePushWithSpecificRemote}
|
||||
isSyncing={isSyncing}
|
||||
onSync={handleSyncWithRemoteSelection}
|
||||
onSyncWithRemote={handleSyncWithSpecificRemote}
|
||||
onSetTracking={handleSetTrackingForRemote}
|
||||
remotes={remotesCache[worktree.path]}
|
||||
onOpenInEditor={handleOpenInEditor}
|
||||
onOpenInIntegratedTerminal={handleOpenInIntegratedTerminal}
|
||||
onOpenInExternalTerminal={handleOpenInExternalTerminal}
|
||||
onViewChanges={handleViewChanges}
|
||||
onViewCommits={handleViewCommits}
|
||||
onDiscardChanges={handleDiscardChanges}
|
||||
onCommit={onCommit}
|
||||
onCreatePR={onCreatePR}
|
||||
onAddressPRComments={onAddressPRComments}
|
||||
onAutoAddressPRComments={onAutoAddressPRComments}
|
||||
onResolveConflicts={onResolveConflicts}
|
||||
onMerge={handleMerge}
|
||||
onDeleteWorktree={onDeleteWorktree}
|
||||
onStartDevServer={handleStartDevServerAndShowLogs}
|
||||
onStopDevServer={handleStopDevServer}
|
||||
onOpenDevServerUrl={handleOpenDevServerUrl}
|
||||
onViewDevServerLogs={handleViewDevServerLogs}
|
||||
onRunInitScript={handleRunInitScript}
|
||||
onToggleAutoMode={handleToggleAutoMode}
|
||||
onStartTests={handleStartTests}
|
||||
onStopTests={handleStopTests}
|
||||
onViewTestLogs={handleViewTestLogs}
|
||||
onStashChanges={handleStashChanges}
|
||||
onViewStashes={handleViewStashes}
|
||||
onCherryPick={handleCherryPick}
|
||||
onAbortOperation={handleAbortOperation}
|
||||
onContinueOperation={handleContinueOperation}
|
||||
hasInitScript={hasInitScript}
|
||||
hasTestCommand={hasTestCommand}
|
||||
terminalScripts={terminalScripts}
|
||||
onRunTerminalScript={handleRunTerminalScript}
|
||||
onEditScripts={handleEditScripts}
|
||||
remotesWithBranch={remotesWithBranch}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={onCreateWorktree}
|
||||
title="Create new worktree"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={async () => {
|
||||
const removedWorktrees = await fetchWorktrees();
|
||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||
onRemovedWorktrees(removedWorktrees);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
title="Refresh worktrees"
|
||||
>
|
||||
{isLoading ? <Spinner size="xs" /> : <RefreshCw className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground"
|
||||
onClick={async () => {
|
||||
const removedWorktrees = await fetchWorktrees();
|
||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||
onRemovedWorktrees(removedWorktrees);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading}
|
||||
title="Refresh worktrees"
|
||||
>
|
||||
{isLoading ? <Spinner size="xs" /> : <RefreshCw className="w-3.5 h-3.5" />}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user