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

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