feat: Add GPT-5 model variants and improve Codex execution logic. Addressed code review comments

This commit is contained in:
gsxdsm
2026-02-18 11:15:38 -08:00
parent d30296d559
commit 5c441f2313
64 changed files with 3628 additions and 2223 deletions

View File

@@ -13,6 +13,7 @@ import {
AlertCircle,
} from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { TruncatedFilePath } from '@/components/ui/truncated-file-path';
import { Button } from './button';
import { useWorktreeDiffs, useGitDiffs } from '@/hooks/queries';
import type { FileStatus } from '@/types/electron';
@@ -299,9 +300,10 @@ function FileDiffSection({
<ChevronRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
)}
<FileText className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<span className="flex-1 text-sm font-mono truncate text-foreground">
{fileDiff.filePath}
</span>
<TruncatedFilePath
path={fileDiff.filePath}
className="flex-1 text-sm font-mono text-foreground"
/>
<div className="flex items-center gap-2 flex-shrink-0">
{fileDiff.isNew && (
<span className="text-xs px-1.5 py-0.5 rounded bg-green-500/20 text-green-400">
@@ -596,9 +598,10 @@ export function GitDiffPanel({
>
<div className="w-full px-3 py-2 flex items-center gap-2 text-left bg-card">
{getFileIcon(file.status)}
<span className="flex-1 text-sm font-mono truncate text-foreground">
{file.path}
</span>
<TruncatedFilePath
path={file.path}
className="flex-1 text-sm font-mono text-foreground"
/>
<span
className={cn(
'text-xs px-1.5 py-0.5 rounded border font-medium',

View File

@@ -0,0 +1,40 @@
import { cn } from '@/lib/utils';
interface TruncatedFilePathProps {
/** The full file path to display */
path: string;
/** Additional CSS class names */
className?: string;
}
/**
* Renders a file path with middle truncation.
*
* When the path is too long to fit in its container, the middle portion
* (directory path) is truncated with an ellipsis while preserving both
* the beginning of the path and the filename at the end.
*
* Example: "src/components/...dialog.tsx" instead of "src/components/views/boa..."
*/
export function TruncatedFilePath({ path, className }: TruncatedFilePathProps) {
const lastSlash = path.lastIndexOf('/');
// If there's no directory component, just render with normal truncation
if (lastSlash === -1) {
return (
<span className={cn('truncate', className)} title={path}>
{path}
</span>
);
}
const dirPart = path.slice(0, lastSlash + 1); // includes trailing slash
const filePart = path.slice(lastSlash + 1);
return (
<span className={cn('flex min-w-0', className)} title={path}>
<span className="truncate flex-shrink">{dirPart}</span>
<span className="flex-shrink-0 whitespace-nowrap">{filePart}</span>
</span>
);
}

View File

@@ -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.

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 || [],

View File

@@ -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',

View File

@@ -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)}`,
});
}
},

View File

@@ -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>

View File

@@ -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

View File

@@ -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({