mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
feat: Add GPT-5 model variants and improve Codex execution logic. Addressed code review comments
This commit is contained in:
@@ -37,6 +37,7 @@ import { BoardBackgroundModal } from '@/components/dialogs/board-background-moda
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAutoMode } from '@/hooks/use-auto-mode';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
import { useWindowState } from '@/hooks/use-window-state';
|
||||
// Board-view specific imports
|
||||
import { BoardHeader } from './board-view/board-header';
|
||||
@@ -868,358 +869,7 @@ export function BoardView() {
|
||||
}
|
||||
}, [currentProject, selectedFeatureIds, loadFeatures, exitSelectionMode]);
|
||||
|
||||
// Handler for addressing PR comments - creates a feature and starts it automatically
|
||||
const handleAddressPRComments = useCallback(
|
||||
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
||||
// Use a simple prompt that instructs the agent to read and address PR feedback
|
||||
// The agent will fetch the PR comments directly, which is more reliable and up-to-date
|
||||
const prNumber = prInfo.number;
|
||||
const description = `Read the review requests on PR #${prNumber} and address any feedback the best you can.`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Address PR #${prNumber} Review Comments`,
|
||||
category: 'PR Review',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: worktree.branch,
|
||||
workMode: 'custom' as const, // Use the worktree's branch
|
||||
priority: 1, // High priority for PR feedback
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create PR comments feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error('Could not find newly created feature to start it automatically.');
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler for resolving conflicts - opens dialog to select remote branch, then creates a feature
|
||||
const handleResolveConflicts = useCallback((worktree: WorktreeInfo) => {
|
||||
setSelectedWorktreeForAction(worktree);
|
||||
setShowMergeRebaseDialog(true);
|
||||
}, []);
|
||||
|
||||
// Handler called when user confirms the merge & rebase dialog
|
||||
const handleConfirmResolveConflicts = useCallback(
|
||||
async (worktree: WorktreeInfo, remoteBranch: string, strategy: PullStrategy) => {
|
||||
const isRebase = strategy === 'rebase';
|
||||
|
||||
const description = isRebase
|
||||
? `Fetch the latest changes from ${remoteBranch} and rebase the current branch (${worktree.branch}) onto ${remoteBranch}. Use "git fetch" followed by "git rebase ${remoteBranch}" to replay commits on top of the remote branch for a linear history. If rebase conflicts arise, resolve them one commit at a time using "git rebase --continue" after fixing each conflict. After completing the rebase, ensure the code compiles and tests pass.`
|
||||
: `Pull latest from ${remoteBranch} and resolve conflicts. Merge ${remoteBranch} into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`;
|
||||
|
||||
const title = isRebase
|
||||
? `Rebase & Resolve Conflicts: ${worktree.branch} onto ${remoteBranch}`
|
||||
: `Resolve Merge Conflicts: ${remoteBranch} → ${worktree.branch}`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: worktree.branch,
|
||||
workMode: 'custom' as const, // Use the worktree's branch
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create resolve conflicts feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error('Could not find newly created feature to start it automatically.');
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when merge/rebase fails due to conflicts and user wants to create a feature to resolve them
|
||||
const handleCreateMergeConflictResolutionFeature = useCallback(
|
||||
async (conflictInfo: MergeConflictInfo) => {
|
||||
const isRebase = conflictInfo.operationType === 'rebase';
|
||||
const conflictFilesInfo =
|
||||
conflictInfo.conflictFiles && conflictInfo.conflictFiles.length > 0
|
||||
? `\n\nConflicting files:\n${conflictInfo.conflictFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const description = isRebase
|
||||
? `Fetch the latest changes from ${conflictInfo.sourceBranch} and rebase the current branch (${conflictInfo.targetBranch}) onto ${conflictInfo.sourceBranch}. Use "git fetch" followed by "git rebase ${conflictInfo.sourceBranch}" to replay commits on top of the remote branch for a linear history. If rebase conflicts arise, resolve them one commit at a time using "git rebase --continue" after fixing each conflict. After completing the rebase, ensure the code compiles and tests pass.${conflictFilesInfo}`
|
||||
: `Resolve merge conflicts when merging "${conflictInfo.sourceBranch}" into "${conflictInfo.targetBranch}". The merge was started but encountered conflicts that need to be resolved manually. After resolving all conflicts, ensure the code compiles and tests pass, then complete the merge by committing the resolved changes.${conflictFilesInfo}`;
|
||||
|
||||
const title = isRebase
|
||||
? `Rebase & Resolve Conflicts: ${conflictInfo.targetBranch} onto ${conflictInfo.sourceBranch}`
|
||||
: `Resolve Merge Conflicts: ${conflictInfo.sourceBranch} → ${conflictInfo.targetBranch}`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.targetBranch,
|
||||
workMode: 'custom' as const, // Use the target branch where conflicts need to be resolved
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create merge conflict resolution feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error('Could not find newly created feature to start it automatically.');
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when branch switch stash reapply causes merge conflicts
|
||||
const handleBranchSwitchConflict = useCallback(
|
||||
async (conflictInfo: BranchSwitchConflictInfo) => {
|
||||
const description = `Resolve merge conflicts that occurred when switching from "${conflictInfo.previousBranch}" to "${conflictInfo.branchName}". Local changes were stashed before switching and reapplying them caused conflicts. Please resolve all merge conflicts, ensure the code compiles and tests pass.`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Resolve Stash Conflicts: switch to ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1,
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create branch switch conflict resolution feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error('Could not find newly created feature to start it automatically.');
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when checkout fails AND the stash-pop restoration produces merge conflicts.
|
||||
// Creates an AI-assisted board task to guide the user through resolving the conflicts.
|
||||
const handleStashPopConflict = useCallback(
|
||||
async (conflictInfo: StashPopConflictInfo) => {
|
||||
const description =
|
||||
`Resolve merge conflicts that occurred when attempting to switch to branch "${conflictInfo.branchName}". ` +
|
||||
`The checkout failed and, while restoring the previously stashed local changes, git reported merge conflicts. ` +
|
||||
`${conflictInfo.stashPopConflictMessage} ` +
|
||||
`Please review all conflicted files, resolve the conflicts, ensure the code compiles and tests pass, ` +
|
||||
`then re-attempt the branch switch.`;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Resolve Stash-Pop Conflicts: branch switch to ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1,
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create stash-pop conflict resolution feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error(
|
||||
'Could not find newly created stash-pop conflict feature to start it automatically.'
|
||||
);
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when stash apply/pop results in merge conflicts and user wants AI resolution
|
||||
const handleStashApplyConflict = useCallback(
|
||||
async (conflictInfo: StashApplyConflictInfo) => {
|
||||
const operationLabel = conflictInfo.operation === 'pop' ? 'popping' : 'applying';
|
||||
const conflictFilesList =
|
||||
conflictInfo.conflictFiles.length > 0
|
||||
? `\n\nConflicted files:\n${conflictInfo.conflictFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const description =
|
||||
`Resolve merge conflicts that occurred when ${operationLabel} stash "${conflictInfo.stashRef}" ` +
|
||||
`on branch "${conflictInfo.branchName}". ` +
|
||||
`The stash was ${conflictInfo.operation === 'pop' ? 'popped' : 'applied'} but resulted in merge conflicts ` +
|
||||
`that need to be resolved. Please review all conflicted files, resolve the conflicts, ` +
|
||||
`ensure the code compiles and tests pass, then commit the resolved changes.` +
|
||||
conflictFilesList;
|
||||
|
||||
// Create the feature
|
||||
const featureData = {
|
||||
title: `Resolve Stash Apply Conflicts: ${conflictInfo.stashRef} on ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
// Capture existing feature IDs before adding
|
||||
const featuresBeforeIds = new Set(useAppStore.getState().features.map((f) => f.id));
|
||||
try {
|
||||
await handleAddFeature(featureData);
|
||||
} catch (error) {
|
||||
logger.error('Failed to create stash apply conflict resolution feature:', error);
|
||||
toast.error('Failed to create feature', {
|
||||
description: error instanceof Error ? error.message : 'An error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the newly created feature by looking for an ID that wasn't in the original set
|
||||
const latestFeatures = useAppStore.getState().features;
|
||||
const newFeature = latestFeatures.find((f) => !featuresBeforeIds.has(f.id));
|
||||
|
||||
if (newFeature) {
|
||||
await handleStartImplementation(newFeature);
|
||||
} else {
|
||||
logger.error(
|
||||
'Could not find newly created stash apply conflict feature to start it automatically.'
|
||||
);
|
||||
toast.error('Failed to auto-start feature', {
|
||||
description: 'The feature was created but could not be started automatically.',
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleAddFeature, handleStartImplementation, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler for "Make" button - creates a feature and immediately starts it
|
||||
// Helper that creates a feature and immediately starts it (used by conflict handlers and the Make button)
|
||||
const handleAddAndStartFeature = useCallback(
|
||||
async (featureData: Parameters<typeof handleAddFeature>[0]) => {
|
||||
// Capture existing feature IDs before adding
|
||||
@@ -1250,6 +900,209 @@ export function BoardView() {
|
||||
[handleAddFeature, handleStartImplementation]
|
||||
);
|
||||
|
||||
// Handler for addressing PR comments - creates a feature and starts it automatically
|
||||
const handleAddressPRComments = useCallback(
|
||||
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
||||
// Use a simple prompt that instructs the agent to read and address PR feedback
|
||||
// The agent will fetch the PR comments directly, which is more reliable and up-to-date
|
||||
const prNumber = prInfo.number;
|
||||
const description = `Read the review requests on PR #${prNumber} and address any feedback the best you can.`;
|
||||
|
||||
const featureData = {
|
||||
title: `Address PR #${prNumber} Review Comments`,
|
||||
category: 'PR Review',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: worktree.branch,
|
||||
workMode: 'custom' as const, // Use the worktree's branch
|
||||
priority: 1, // High priority for PR feedback
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler for resolving conflicts - opens dialog to select remote branch, then creates a feature
|
||||
const handleResolveConflicts = useCallback((worktree: WorktreeInfo) => {
|
||||
setSelectedWorktreeForAction(worktree);
|
||||
setShowMergeRebaseDialog(true);
|
||||
}, []);
|
||||
|
||||
// Handler called when user confirms the merge & rebase dialog
|
||||
const handleConfirmResolveConflicts = useCallback(
|
||||
async (worktree: WorktreeInfo, remoteBranch: string, strategy: PullStrategy) => {
|
||||
const isRebase = strategy === 'rebase';
|
||||
|
||||
const description = isRebase
|
||||
? `Fetch the latest changes from ${remoteBranch} and rebase the current branch (${worktree.branch}) onto ${remoteBranch}. Use "git fetch" followed by "git rebase ${remoteBranch}" to replay commits on top of the remote branch for a linear history. If rebase conflicts arise, resolve them one commit at a time using "git rebase --continue" after fixing each conflict. After completing the rebase, ensure the code compiles and tests pass.`
|
||||
: `Pull latest from ${remoteBranch} and resolve conflicts. Merge ${remoteBranch} into the current branch (${worktree.branch}), resolving any merge conflicts that arise. After resolving conflicts, ensure the code compiles and tests pass.`;
|
||||
|
||||
const title = isRebase
|
||||
? `Rebase & Resolve Conflicts: ${worktree.branch} onto ${remoteBranch}`
|
||||
: `Resolve Merge Conflicts: ${remoteBranch} → ${worktree.branch}`;
|
||||
|
||||
const featureData = {
|
||||
title,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: worktree.branch,
|
||||
workMode: 'custom' as const, // Use the worktree's branch
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when merge/rebase fails due to conflicts and user wants to create a feature to resolve them
|
||||
const handleCreateMergeConflictResolutionFeature = useCallback(
|
||||
async (conflictInfo: MergeConflictInfo) => {
|
||||
const isRebase = conflictInfo.operationType === 'rebase';
|
||||
const conflictFilesInfo =
|
||||
conflictInfo.conflictFiles && conflictInfo.conflictFiles.length > 0
|
||||
? `\n\nConflicting files:\n${conflictInfo.conflictFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const description = isRebase
|
||||
? `Fetch the latest changes from ${conflictInfo.sourceBranch} and rebase the current branch (${conflictInfo.targetBranch}) onto ${conflictInfo.sourceBranch}. Use "git fetch" followed by "git rebase ${conflictInfo.sourceBranch}" to replay commits on top of the remote branch for a linear history. If rebase conflicts arise, resolve them one commit at a time using "git rebase --continue" after fixing each conflict. After completing the rebase, ensure the code compiles and tests pass.${conflictFilesInfo}`
|
||||
: `Resolve merge conflicts when merging "${conflictInfo.sourceBranch}" into "${conflictInfo.targetBranch}". The merge was started but encountered conflicts that need to be resolved manually. After resolving all conflicts, ensure the code compiles and tests pass, then complete the merge by committing the resolved changes.${conflictFilesInfo}`;
|
||||
|
||||
const title = isRebase
|
||||
? `Rebase & Resolve Conflicts: ${conflictInfo.targetBranch} onto ${conflictInfo.sourceBranch}`
|
||||
: `Resolve Merge Conflicts: ${conflictInfo.sourceBranch} → ${conflictInfo.targetBranch}`;
|
||||
|
||||
const featureData = {
|
||||
title,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: 'opus' as const,
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.targetBranch,
|
||||
workMode: 'custom' as const, // Use the target branch where conflicts need to be resolved
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when branch switch stash reapply causes merge conflicts
|
||||
const handleBranchSwitchConflict = useCallback(
|
||||
async (conflictInfo: BranchSwitchConflictInfo) => {
|
||||
const description = `Resolve merge conflicts that occurred when switching from "${conflictInfo.previousBranch}" to "${conflictInfo.branchName}". Local changes were stashed before switching and reapplying them caused conflicts. Please resolve all merge conflicts, ensure the code compiles and tests pass.`;
|
||||
|
||||
const featureData = {
|
||||
title: `Resolve Stash Conflicts: switch to ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: resolveModelString('opus'),
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1,
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when checkout fails AND the stash-pop restoration produces merge conflicts.
|
||||
// Creates an AI-assisted board task to guide the user through resolving the conflicts.
|
||||
const handleStashPopConflict = useCallback(
|
||||
async (conflictInfo: StashPopConflictInfo) => {
|
||||
const description =
|
||||
`Resolve merge conflicts that occurred when attempting to switch to branch "${conflictInfo.branchName}". ` +
|
||||
`The checkout failed and, while restoring the previously stashed local changes, git reported merge conflicts. ` +
|
||||
`${conflictInfo.stashPopConflictMessage} ` +
|
||||
`Please review all conflicted files, resolve the conflicts, ensure the code compiles and tests pass, ` +
|
||||
`then re-attempt the branch switch.`;
|
||||
|
||||
const featureData = {
|
||||
title: `Resolve Stash-Pop Conflicts: branch switch to ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: resolveModelString('opus'),
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1,
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// Handler called when stash apply/pop results in merge conflicts and user wants AI resolution
|
||||
const handleStashApplyConflict = useCallback(
|
||||
async (conflictInfo: StashApplyConflictInfo) => {
|
||||
const operationLabel = conflictInfo.operation === 'pop' ? 'popping' : 'applying';
|
||||
const conflictFilesList =
|
||||
conflictInfo.conflictFiles.length > 0
|
||||
? `\n\nConflicted files:\n${conflictInfo.conflictFiles.map((f) => `- ${f}`).join('\n')}`
|
||||
: '';
|
||||
|
||||
const description =
|
||||
`Resolve merge conflicts that occurred when ${operationLabel} stash "${conflictInfo.stashRef}" ` +
|
||||
`on branch "${conflictInfo.branchName}". ` +
|
||||
`The stash was ${conflictInfo.operation === 'pop' ? 'popped' : 'applied'} but resulted in merge conflicts ` +
|
||||
`that need to be resolved. Please review all conflicted files, resolve the conflicts, ` +
|
||||
`ensure the code compiles and tests pass, then commit the resolved changes.` +
|
||||
conflictFilesList;
|
||||
|
||||
const featureData = {
|
||||
title: `Resolve Stash Apply Conflicts: ${conflictInfo.stashRef} on ${conflictInfo.branchName}`,
|
||||
category: 'Maintenance',
|
||||
description,
|
||||
images: [],
|
||||
imagePaths: [],
|
||||
skipTests: defaultSkipTests,
|
||||
model: resolveModelString('opus'),
|
||||
thinkingLevel: 'none' as const,
|
||||
branchName: conflictInfo.branchName,
|
||||
workMode: 'custom' as const,
|
||||
priority: 1, // High priority for conflict resolution
|
||||
planningMode: 'skip' as const,
|
||||
requirePlanApproval: false,
|
||||
};
|
||||
|
||||
await handleAddAndStartFeature(featureData);
|
||||
},
|
||||
[handleAddAndStartFeature, defaultSkipTests]
|
||||
);
|
||||
|
||||
// NOTE: Auto mode polling loop has been moved to the backend.
|
||||
// The frontend now just toggles the backend's auto loop via API calls.
|
||||
// See use-auto-mode.ts for the start/stop logic that calls the backend.
|
||||
|
||||
@@ -27,6 +27,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TruncatedFilePath } from '@/components/ui/truncated-file-path';
|
||||
import type { FileStatus } from '@/types/electron';
|
||||
|
||||
interface WorktreeInfo {
|
||||
@@ -566,9 +567,10 @@ export function CommitWorktreeDialog({
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
{getFileIcon(file.status)}
|
||||
<span className="text-xs font-mono truncate flex-1 text-foreground">
|
||||
{file.path}
|
||||
</span>
|
||||
<TruncatedFilePath
|
||||
path={file.path}
|
||||
className="text-xs font-mono flex-1 text-foreground"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] px-1.5 py-0.5 rounded border font-medium flex-shrink-0',
|
||||
|
||||
@@ -26,6 +26,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TruncatedFilePath } from '@/components/ui/truncated-file-path';
|
||||
import type { FileStatus } from '@/types/electron';
|
||||
|
||||
interface WorktreeInfo {
|
||||
@@ -313,9 +314,12 @@ export function DiscardWorktreeChangesDialog({
|
||||
const result = await api.git.getDiffs(worktree.path);
|
||||
if (result.success) {
|
||||
const fileList = result.files ?? [];
|
||||
if (!cancelled) setError(null);
|
||||
if (!cancelled) setFiles(fileList);
|
||||
if (!cancelled) setDiffContent(result.diff ?? '');
|
||||
if (!cancelled) setSelectedFiles(new Set());
|
||||
} else {
|
||||
if (!cancelled) setError(result.error || 'Failed to fetch diffs');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -495,9 +499,10 @@ export function DiscardWorktreeChangesDialog({
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
{getFileIcon(file.status)}
|
||||
<span className="text-xs font-mono truncate flex-1 text-foreground">
|
||||
{file.path}
|
||||
</span>
|
||||
<TruncatedFilePath
|
||||
path={file.path}
|
||||
className="text-xs font-mono flex-1 text-foreground"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] px-1.5 py-0.5 rounded border font-medium flex-shrink-0',
|
||||
|
||||
@@ -42,6 +42,7 @@ type PullPhase =
|
||||
|
||||
interface PullResult {
|
||||
branch: string;
|
||||
remote?: string;
|
||||
pulled: boolean;
|
||||
message: string;
|
||||
hasLocalChanges?: boolean;
|
||||
@@ -115,6 +116,11 @@ export function GitPullDialog({
|
||||
setPullResult(result.result);
|
||||
setPhase('success');
|
||||
onPulled?.();
|
||||
} else {
|
||||
// Unexpected response: success but no recognizable fields
|
||||
setPullResult(result.result ?? null);
|
||||
setErrorMessage('Unexpected pull response');
|
||||
setPhase('error');
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Failed to check for changes');
|
||||
@@ -160,8 +166,9 @@ export function GitPullDialog({
|
||||
const handleResolveWithAI = useCallback(() => {
|
||||
if (!worktree || !pullResult || !onCreateConflictResolutionFeature) return;
|
||||
|
||||
const effectiveRemote = pullResult.remote || remote;
|
||||
const conflictInfo: MergeConflictInfo = {
|
||||
sourceBranch: `${remote || 'origin'}/${pullResult.branch}`,
|
||||
sourceBranch: effectiveRemote ? `${effectiveRemote}/${pullResult.branch}` : pullResult.branch,
|
||||
targetBranch: pullResult.branch,
|
||||
targetWorktreePath: worktree.path,
|
||||
conflictFiles: pullResult.conflictFiles || [],
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Spinner } from '@/components/ui/spinner';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TruncatedFilePath } from '@/components/ui/truncated-file-path';
|
||||
import type { FileStatus } from '@/types/electron';
|
||||
|
||||
interface WorktreeInfo {
|
||||
@@ -514,9 +515,10 @@ export function StashChangesDialog({
|
||||
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
||||
)}
|
||||
{getFileIcon(file.status)}
|
||||
<span className="text-xs font-mono truncate flex-1 text-foreground">
|
||||
{file.path}
|
||||
</span>
|
||||
<TruncatedFilePath
|
||||
path={file.path}
|
||||
className="text-xs font-mono flex-1 text-foreground"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-[10px] px-1.5 py-0.5 rounded border font-medium flex-shrink-0',
|
||||
|
||||
@@ -930,6 +930,7 @@ export function useBoardActions({
|
||||
// - If the feature had a branch assigned, keep it (preserves worktree context)
|
||||
// - If no branch was assigned, it will show on the primary worktree
|
||||
const featureBranch = feature.branchName;
|
||||
const branchLabel = featureBranch ?? 'primary worktree';
|
||||
|
||||
// Check if the feature will be visible on the current worktree view
|
||||
const willBeVisibleOnCurrentView = !featureBranch
|
||||
@@ -949,7 +950,7 @@ export function useBoardActions({
|
||||
});
|
||||
} else {
|
||||
toast.success('Feature restored', {
|
||||
description: `Moved back to verified on branch "${featureBranch}": ${truncateDescription(feature.description)}`,
|
||||
description: `Moved back to verified on branch "${branchLabel}": ${truncateDescription(feature.description)}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -197,11 +197,23 @@ export function WorktreeActionsDropdown({
|
||||
|
||||
// Check git operations availability
|
||||
const canPerformGitOps = gitRepoStatus.isGitRepo && gitRepoStatus.hasCommits;
|
||||
const gitOpsDisabledReason = !gitRepoStatus.isGitRepo
|
||||
? 'Not a git repository'
|
||||
: !gitRepoStatus.hasCommits
|
||||
? 'Repository has no commits yet'
|
||||
: null;
|
||||
// While git status is loading, treat git ops as unavailable to avoid stale state enabling actions
|
||||
const isGitOpsAvailable = !isLoadingGitStatus && canPerformGitOps;
|
||||
const gitOpsDisabledReason = isLoadingGitStatus
|
||||
? 'Checking git status...'
|
||||
: !gitRepoStatus.isGitRepo
|
||||
? 'Not a git repository'
|
||||
: !gitRepoStatus.hasCommits
|
||||
? 'Repository has no commits yet'
|
||||
: null;
|
||||
|
||||
// Determine if the changes/PR section has any visible items
|
||||
const showCreatePR = (!worktree.isMain || worktree.hasChanges) && !hasPR;
|
||||
const showPRInfo = hasPR && !!worktree.pr;
|
||||
const hasChangesSectionContent = worktree.hasChanges || showCreatePR || showPRInfo;
|
||||
|
||||
// Determine if the destructive/bottom section has any visible items
|
||||
const hasDestructiveSectionContent = worktree.hasChanges || !worktree.isMain;
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={onOpenChange}>
|
||||
@@ -232,7 +244,7 @@ export function WorktreeActionsDropdown({
|
||||
</>
|
||||
)}
|
||||
{/* Warning label when git operations are not available (only show once loaded) */}
|
||||
{!isLoadingGitStatus && !canPerformGitOps && (
|
||||
{!isLoadingGitStatus && !isGitOpsAvailable && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-xs flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
@@ -362,14 +374,16 @@ export function WorktreeActionsDropdown({
|
||||
)}
|
||||
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onPull(worktree)}
|
||||
disabled={isPulling || !canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
onClick={() => isGitOpsAvailable && onPull(worktree)}
|
||||
disabled={isPulling || !isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<Download className={cn('w-3.5 h-3.5 mr-2', isPulling && 'animate-pulse')} />
|
||||
{isPulling ? 'Pulling...' : 'Pull'}
|
||||
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
|
||||
{canPerformGitOps && behindCount > 0 && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
{isGitOpsAvailable && behindCount > 0 && (
|
||||
<span className="ml-auto text-[10px] bg-muted px-1.5 py-0.5 rounded">
|
||||
{behindCount} behind
|
||||
</span>
|
||||
@@ -379,26 +393,28 @@ export function WorktreeActionsDropdown({
|
||||
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!canPerformGitOps) return;
|
||||
if (!isGitOpsAvailable) return;
|
||||
if (!hasRemoteBranch) {
|
||||
onPushNewBranch(worktree);
|
||||
} else {
|
||||
onPush(worktree);
|
||||
}
|
||||
}}
|
||||
disabled={isPushing || (hasRemoteBranch && aheadCount === 0) || !canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
disabled={isPushing || (hasRemoteBranch && aheadCount === 0) || !isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<Upload className={cn('w-3.5 h-3.5 mr-2', isPushing && 'animate-pulse')} />
|
||||
{isPushing ? 'Pushing...' : 'Push'}
|
||||
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
|
||||
{canPerformGitOps && !hasRemoteBranch && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
{isGitOpsAvailable && !hasRemoteBranch && (
|
||||
<span className="ml-auto inline-flex items-center gap-0.5 text-[10px] bg-amber-500/20 text-amber-600 dark:text-amber-400 px-1.5 py-0.5 rounded">
|
||||
<CloudOff className="w-2.5 h-2.5" />
|
||||
local only
|
||||
</span>
|
||||
)}
|
||||
{canPerformGitOps && hasRemoteBranch && aheadCount > 0 && (
|
||||
{isGitOpsAvailable && hasRemoteBranch && aheadCount > 0 && (
|
||||
<span className="ml-auto text-[10px] bg-primary/20 text-primary px-1.5 py-0.5 rounded">
|
||||
{aheadCount} ahead
|
||||
</span>
|
||||
@@ -407,27 +423,31 @@ export function WorktreeActionsDropdown({
|
||||
</TooltipWrapper>
|
||||
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onResolveConflicts(worktree)}
|
||||
disabled={!canPerformGitOps}
|
||||
onClick={() => isGitOpsAvailable && onResolveConflicts(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn(
|
||||
'text-xs text-purple-500 focus:text-purple-600',
|
||||
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<GitMerge className="w-3.5 h-3.5 mr-2" />
|
||||
Merge & Rebase
|
||||
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
<TooltipWrapper showTooltip={!!gitOpsDisabledReason} tooltipContent={gitOpsDisabledReason}>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onViewCommits(worktree)}
|
||||
disabled={!canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
onClick={() => isGitOpsAvailable && onViewCommits(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<History className="w-3.5 h-3.5 mr-2" />
|
||||
View Commits
|
||||
{!canPerformGitOps && <AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />}
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
{/* Cherry-pick commits from another branch */}
|
||||
@@ -437,13 +457,13 @@ export function WorktreeActionsDropdown({
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onCherryPick(worktree)}
|
||||
disabled={!canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
onClick={() => isGitOpsAvailable && onCherryPick(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<Cherry className="w-3.5 h-3.5 mr-2" />
|
||||
Cherry Pick
|
||||
{!canPerformGitOps && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -451,7 +471,7 @@ export function WorktreeActionsDropdown({
|
||||
)}
|
||||
{/* Stash operations - combined submenu or simple item */}
|
||||
{(onStashChanges || onViewStashes) && (
|
||||
<TooltipWrapper showTooltip={!canPerformGitOps} tooltipContent={gitOpsDisabledReason}>
|
||||
<TooltipWrapper showTooltip={!isGitOpsAvailable} tooltipContent={gitOpsDisabledReason}>
|
||||
{onViewStashes && worktree.hasChanges && onStashChanges ? (
|
||||
// Both "Stash Changes" (primary) and "View Stashes" (secondary) are available - show split submenu
|
||||
<DropdownMenuSub>
|
||||
@@ -459,18 +479,18 @@ export function WorktreeActionsDropdown({
|
||||
{/* Main clickable area - stash changes (primary action) */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!canPerformGitOps) return;
|
||||
if (!isGitOpsAvailable) return;
|
||||
onStashChanges(worktree);
|
||||
}}
|
||||
disabled={!canPerformGitOps}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn(
|
||||
'text-xs flex-1 pr-0 rounded-r-none',
|
||||
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Archive className="w-3.5 h-3.5 mr-2" />
|
||||
Stash Changes
|
||||
{!canPerformGitOps && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -478,9 +498,9 @@ export function WorktreeActionsDropdown({
|
||||
<DropdownMenuSubTrigger
|
||||
className={cn(
|
||||
'text-xs px-1 rounded-l-none border-l border-border/30 h-8',
|
||||
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
disabled={!canPerformGitOps}
|
||||
disabled={!isGitOpsAvailable}
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -494,19 +514,19 @@ export function WorktreeActionsDropdown({
|
||||
// Only one action is meaningful - render a simple menu item without submenu
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!canPerformGitOps) return;
|
||||
if (!isGitOpsAvailable) return;
|
||||
if (worktree.hasChanges && onStashChanges) {
|
||||
onStashChanges(worktree);
|
||||
} else if (onViewStashes) {
|
||||
onViewStashes(worktree);
|
||||
}
|
||||
}}
|
||||
disabled={!canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<Archive className="w-3.5 h-3.5 mr-2" />
|
||||
{worktree.hasChanges && onStashChanges ? 'Stash Changes' : 'Stashes'}
|
||||
{!canPerformGitOps && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -639,7 +659,7 @@ export function WorktreeActionsDropdown({
|
||||
Re-run Init Script
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{(hasChangesSectionContent || hasDestructiveSectionContent) && <DropdownMenuSeparator />}
|
||||
|
||||
{worktree.hasChanges && (
|
||||
<DropdownMenuItem onClick={() => onViewChanges(worktree)} className="text-xs">
|
||||
@@ -649,43 +669,43 @@ export function WorktreeActionsDropdown({
|
||||
)}
|
||||
{worktree.hasChanges && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!gitRepoStatus.isGitRepo}
|
||||
tooltipContent="Not a git repository"
|
||||
showTooltip={!!gitOpsDisabledReason}
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => gitRepoStatus.isGitRepo && onCommit(worktree)}
|
||||
disabled={!gitRepoStatus.isGitRepo}
|
||||
className={cn('text-xs', !gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed')}
|
||||
onClick={() => isGitOpsAvailable && onCommit(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<GitCommit className="w-3.5 h-3.5 mr-2" />
|
||||
Commit Changes
|
||||
{!gitRepoStatus.isGitRepo && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{/* Show PR option for non-primary worktrees, or primary worktree with changes */}
|
||||
{(!worktree.isMain || worktree.hasChanges) && !hasPR && (
|
||||
{showCreatePR && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!!gitOpsDisabledReason}
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onCreatePR(worktree)}
|
||||
disabled={!canPerformGitOps}
|
||||
className={cn('text-xs', !canPerformGitOps && 'opacity-50 cursor-not-allowed')}
|
||||
onClick={() => isGitOpsAvailable && onCreatePR(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn('text-xs', !isGitOpsAvailable && 'opacity-50 cursor-not-allowed')}
|
||||
>
|
||||
<GitPullRequest className="w-3.5 h-3.5 mr-2" />
|
||||
Create Pull Request
|
||||
{!canPerformGitOps && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{/* Show PR info and Address Comments button if PR exists */}
|
||||
{hasPR && worktree.pr && (
|
||||
{showPRInfo && worktree.pr && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
@@ -722,23 +742,23 @@ export function WorktreeActionsDropdown({
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{hasChangesSectionContent && hasDestructiveSectionContent && <DropdownMenuSeparator />}
|
||||
{worktree.hasChanges && (
|
||||
<TooltipWrapper
|
||||
showTooltip={!gitRepoStatus.isGitRepo}
|
||||
tooltipContent="Not a git repository"
|
||||
showTooltip={!!gitOpsDisabledReason}
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => gitRepoStatus.isGitRepo && onDiscardChanges(worktree)}
|
||||
disabled={!gitRepoStatus.isGitRepo}
|
||||
onClick={() => isGitOpsAvailable && onDiscardChanges(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn(
|
||||
'text-xs text-destructive focus:text-destructive',
|
||||
!gitRepoStatus.isGitRepo && 'opacity-50 cursor-not-allowed'
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<Undo2 className="w-3.5 h-3.5 mr-2" />
|
||||
Discard Changes
|
||||
{!gitRepoStatus.isGitRepo && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
@@ -751,16 +771,16 @@ export function WorktreeActionsDropdown({
|
||||
tooltipContent={gitOpsDisabledReason}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => canPerformGitOps && onMerge(worktree)}
|
||||
disabled={!canPerformGitOps}
|
||||
onClick={() => isGitOpsAvailable && onMerge(worktree)}
|
||||
disabled={!isGitOpsAvailable}
|
||||
className={cn(
|
||||
'text-xs text-green-600 focus:text-green-700',
|
||||
!canPerformGitOps && 'opacity-50 cursor-not-allowed'
|
||||
!isGitOpsAvailable && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
<GitMerge className="w-3.5 h-3.5 mr-2" />
|
||||
Merge Branch
|
||||
{!canPerformGitOps && (
|
||||
{!isGitOpsAvailable && (
|
||||
<AlertCircle className="w-3 h-3 ml-auto text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -633,14 +633,14 @@ export function WorktreePanel({
|
||||
setPullDialogRemote(remote);
|
||||
setPullDialogWorktree(worktree);
|
||||
setPullDialogOpen(true);
|
||||
await handlePull(worktree, remote);
|
||||
await _handlePull(worktree, remote);
|
||||
} else {
|
||||
await handlePush(worktree, remote);
|
||||
}
|
||||
fetchBranches(worktree.path);
|
||||
fetchWorktrees();
|
||||
},
|
||||
[selectRemoteOperation, handlePush, fetchBranches, fetchWorktrees]
|
||||
[selectRemoteOperation, _handlePull, handlePush, fetchBranches, fetchWorktrees]
|
||||
);
|
||||
|
||||
// Handle confirming the push to remote dialog
|
||||
|
||||
@@ -32,6 +32,11 @@ const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
|
||||
label: 'GPT-5.3-Codex',
|
||||
description: 'Latest frontier agentic coding model',
|
||||
},
|
||||
'codex-gpt-5.3-codex-spark': {
|
||||
id: 'codex-gpt-5.3-codex-spark',
|
||||
label: 'GPT-5.3-Codex-Spark',
|
||||
description: 'Near-instant real-time coding model, 1000+ tokens/sec',
|
||||
},
|
||||
'codex-gpt-5.2-codex': {
|
||||
id: 'codex-gpt-5.2-codex',
|
||||
label: 'GPT-5.2-Codex',
|
||||
@@ -47,6 +52,21 @@ const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
|
||||
label: 'GPT-5.1-Codex-Mini',
|
||||
description: 'Optimized for codex. Cheaper, faster, but less capable',
|
||||
},
|
||||
'codex-gpt-5.1-codex': {
|
||||
id: 'codex-gpt-5.1-codex',
|
||||
label: 'GPT-5.1-Codex',
|
||||
description: 'Original GPT-5.1 Codex agentic coding model',
|
||||
},
|
||||
'codex-gpt-5-codex': {
|
||||
id: 'codex-gpt-5-codex',
|
||||
label: 'GPT-5-Codex',
|
||||
description: 'Original GPT-5 Codex model',
|
||||
},
|
||||
'codex-gpt-5-codex-mini': {
|
||||
id: 'codex-gpt-5-codex-mini',
|
||||
label: 'GPT-5-Codex-Mini',
|
||||
description: 'Smaller, cheaper GPT-5 Codex variant',
|
||||
},
|
||||
'codex-gpt-5.2': {
|
||||
id: 'codex-gpt-5.2',
|
||||
label: 'GPT-5.2',
|
||||
@@ -57,6 +77,11 @@ const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
|
||||
label: 'GPT-5.1',
|
||||
description: 'Great for coding and agentic tasks across domains',
|
||||
},
|
||||
'codex-gpt-5': {
|
||||
id: 'codex-gpt-5',
|
||||
label: 'GPT-5',
|
||||
description: 'Base GPT-5 model via Codex',
|
||||
},
|
||||
};
|
||||
|
||||
export function CodexModelConfiguration({
|
||||
|
||||
Reference in New Issue
Block a user