feat: implement bulk feature verification and enhance selection mode

- Added functionality for bulk verifying features in the BoardView, allowing users to mark multiple features as verified at once.
- Introduced a selection target mechanism to differentiate between 'backlog' and 'waiting_approval' features during selection mode.
- Updated the KanbanCard and SelectionActionBar components to support the new selection target logic, improving user experience for bulk actions.
- Enhanced the UI to provide appropriate actions based on the current selection target, including verification options for waiting approval features.
This commit is contained in:
webdevcody
2026-01-15 13:14:15 -05:00
parent 241fd0b252
commit 06b047cfcb
10 changed files with 348 additions and 73 deletions

View File

@@ -30,19 +30,27 @@ export function createBulkDeleteHandler(featureLoader: FeatureLoader) {
return; return;
} }
const results = await Promise.all( // Process in parallel batches of 20 for efficiency
featureIds.map(async (featureId) => { const BATCH_SIZE = 20;
const success = await featureLoader.delete(projectPath, featureId); const results: BulkDeleteResult[] = [];
if (success) {
return { featureId, success: true }; for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
} const batch = featureIds.slice(i, i + BATCH_SIZE);
return { const batchResults = await Promise.all(
featureId, batch.map(async (featureId) => {
success: false, const success = await featureLoader.delete(projectPath, featureId);
error: 'Deletion failed. Check server logs for details.', if (success) {
}; return { featureId, success: true };
}) }
); return {
featureId,
success: false,
error: 'Deletion failed. Check server logs for details.',
};
})
);
results.push(...batchResults);
}
const successCount = results.reduce((count, r) => count + (r.success ? 1 : 0), 0); const successCount = results.reduce((count, r) => count + (r.success ? 1 : 0), 0);
const failureCount = results.length - successCount; const failureCount = results.length - successCount;

View File

@@ -43,17 +43,36 @@ export function createBulkUpdateHandler(featureLoader: FeatureLoader) {
const results: BulkUpdateResult[] = []; const results: BulkUpdateResult[] = [];
const updatedFeatures: Feature[] = []; const updatedFeatures: Feature[] = [];
for (const featureId of featureIds) { // Process in parallel batches of 20 for efficiency
try { const BATCH_SIZE = 20;
const updated = await featureLoader.update(projectPath, featureId, updates); for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
results.push({ featureId, success: true }); const batch = featureIds.slice(i, i + BATCH_SIZE);
updatedFeatures.push(updated); const batchResults = await Promise.all(
} catch (error) { batch.map(async (featureId) => {
results.push({ try {
featureId, const updated = await featureLoader.update(projectPath, featureId, updates);
success: false, return { featureId, success: true as const, feature: updated };
error: getErrorMessage(error), } catch (error) {
}); return {
featureId,
success: false as const,
error: getErrorMessage(error),
};
}
})
);
for (const result of batchResults) {
if (result.success) {
results.push({ featureId: result.featureId, success: true });
updatedFeatures.push(result.feature);
} else {
results.push({
featureId: result.featureId,
success: false,
error: result.error,
});
}
} }
} }

View File

@@ -187,6 +187,7 @@ export function BoardView() {
// Selection mode hook for mass editing // Selection mode hook for mass editing
const { const {
isSelectionMode, isSelectionMode,
selectionTarget,
selectedFeatureIds, selectedFeatureIds,
selectedCount, selectedCount,
toggleSelectionMode, toggleSelectionMode,
@@ -684,6 +685,67 @@ export function BoardView() {
isPrimaryWorktreeBranch, isPrimaryWorktreeBranch,
]); ]);
// Get waiting_approval feature IDs in current branch for "Select All"
const allSelectableWaitingApprovalFeatureIds = useMemo(() => {
return hookFeatures
.filter((f) => {
// Only waiting_approval features
if (f.status !== 'waiting_approval') return false;
// Filter by current worktree branch
const featureBranch = f.branchName;
if (!featureBranch) {
// No branch assigned - only selectable on primary worktree
return currentWorktreePath === null;
}
if (currentWorktreeBranch === null) {
// Viewing main but branch hasn't been initialized
return currentProject?.path
? isPrimaryWorktreeBranch(currentProject.path, featureBranch)
: false;
}
// Match by branch name
return featureBranch === currentWorktreeBranch;
})
.map((f) => f.id);
}, [
hookFeatures,
currentWorktreePath,
currentWorktreeBranch,
currentProject?.path,
isPrimaryWorktreeBranch,
]);
// Handler for bulk verifying multiple features
const handleBulkVerify = useCallback(async () => {
if (!currentProject || selectedFeatureIds.size === 0) return;
try {
const api = getHttpApiClient();
const featureIds = Array.from(selectedFeatureIds);
const updates = { status: 'verified' as const };
// Use bulk update API for efficient batch processing
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
if (result.success) {
// Update local state for all features
featureIds.forEach((featureId) => {
updateFeature(featureId, updates);
});
toast.success(`Verified ${result.updatedCount} features`);
exitSelectionMode();
} else {
toast.error('Failed to verify some features', {
description: `${result.failedCount} features failed to verify`,
});
}
} catch (error) {
logger.error('Bulk verify failed:', error);
toast.error('Failed to verify features');
}
}, [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]);
// Handler for addressing PR comments - creates a feature and starts it automatically // Handler for addressing PR comments - creates a feature and starts it automatically
const handleAddressPRComments = useCallback( const handleAddressPRComments = useCallback(
async (worktree: WorktreeInfo, prInfo: PRInfo) => { async (worktree: WorktreeInfo, prInfo: PRInfo) => {
@@ -1448,6 +1510,7 @@ export function BoardView() {
pipelineConfig={pipelineConfig} pipelineConfig={pipelineConfig}
onOpenPipelineSettings={() => setShowPipelineSettings(true)} onOpenPipelineSettings={() => setShowPipelineSettings(true)}
isSelectionMode={isSelectionMode} isSelectionMode={isSelectionMode}
selectionTarget={selectionTarget}
selectedFeatureIds={selectedFeatureIds} selectedFeatureIds={selectedFeatureIds}
onToggleFeatureSelection={toggleFeatureSelection} onToggleFeatureSelection={toggleFeatureSelection}
onToggleSelectionMode={toggleSelectionMode} onToggleSelectionMode={toggleSelectionMode}
@@ -1463,11 +1526,23 @@ export function BoardView() {
{isSelectionMode && ( {isSelectionMode && (
<SelectionActionBar <SelectionActionBar
selectedCount={selectedCount} selectedCount={selectedCount}
totalCount={allSelectableFeatureIds.length} totalCount={
onEdit={() => setShowMassEditDialog(true)} selectionTarget === 'waiting_approval'
onDelete={handleBulkDelete} ? allSelectableWaitingApprovalFeatureIds.length
: allSelectableFeatureIds.length
}
onEdit={selectionTarget === 'backlog' ? () => setShowMassEditDialog(true) : undefined}
onDelete={selectionTarget === 'backlog' ? handleBulkDelete : undefined}
onVerify={selectionTarget === 'waiting_approval' ? handleBulkVerify : undefined}
onClear={clearSelection} onClear={clearSelection}
onSelectAll={() => selectAll(allSelectableFeatureIds)} onSelectAll={() =>
selectAll(
selectionTarget === 'waiting_approval'
? allSelectableWaitingApprovalFeatureIds
: allSelectableFeatureIds
)
}
mode={selectionTarget === 'waiting_approval' ? 'waiting_approval' : 'backlog'}
/> />
)} )}

View File

@@ -65,6 +65,7 @@ interface KanbanCardProps {
isSelectionMode?: boolean; isSelectionMode?: boolean;
isSelected?: boolean; isSelected?: boolean;
onToggleSelect?: () => void; onToggleSelect?: () => void;
selectionTarget?: 'backlog' | 'waiting_approval' | null;
} }
export const KanbanCard = memo(function KanbanCard({ export const KanbanCard = memo(function KanbanCard({
@@ -96,6 +97,7 @@ export const KanbanCard = memo(function KanbanCard({
isSelectionMode = false, isSelectionMode = false,
isSelected = false, isSelected = false,
onToggleSelect, onToggleSelect,
selectionTarget = null,
}: KanbanCardProps) { }: KanbanCardProps) {
const { useWorktrees } = useAppStore(); const { useWorktrees } = useAppStore();
const [isLifted, setIsLifted] = useState(false); const [isLifted, setIsLifted] = useState(false);
@@ -125,8 +127,8 @@ export const KanbanCard = memo(function KanbanCard({
const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity); const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity);
// Only allow selection for backlog features // Only allow selection for features matching the selection target
const isSelectable = isSelectionMode && feature.status === 'backlog'; const isSelectable = isSelectionMode && feature.status === selectionTarget;
const wrapperClasses = cn( const wrapperClasses = cn(
'relative select-none outline-none touch-none transition-transform duration-200 ease-out', 'relative select-none outline-none touch-none transition-transform duration-200 ease-out',
@@ -180,7 +182,7 @@ export const KanbanCard = memo(function KanbanCard({
{/* Category row with selection checkbox */} {/* Category row with selection checkbox */}
<div className="px-3 pt-3 flex items-center gap-2"> <div className="px-3 pt-3 flex items-center gap-2">
{isSelectionMode && !isOverlay && feature.status === 'backlog' && ( {isSelectable && !isOverlay && (
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onCheckedChange={() => onToggleSelect?.()} onCheckedChange={() => onToggleSelect?.()}

View File

@@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Pencil, X, CheckSquare, Trash2 } from 'lucide-react'; import { Pencil, X, CheckSquare, Trash2, CheckCircle2 } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { import {
Dialog, Dialog,
@@ -11,13 +11,17 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
export type SelectionActionMode = 'backlog' | 'waiting_approval';
interface SelectionActionBarProps { interface SelectionActionBarProps {
selectedCount: number; selectedCount: number;
totalCount: number; totalCount: number;
onEdit: () => void; onEdit?: () => void;
onDelete: () => void; onDelete?: () => void;
onVerify?: () => void;
onClear: () => void; onClear: () => void;
onSelectAll: () => void; onSelectAll: () => void;
mode?: SelectionActionMode;
} }
export function SelectionActionBar({ export function SelectionActionBar({
@@ -25,10 +29,13 @@ export function SelectionActionBar({
totalCount, totalCount,
onEdit, onEdit,
onDelete, onDelete,
onVerify,
onClear, onClear,
onSelectAll, onSelectAll,
mode = 'backlog',
}: SelectionActionBarProps) { }: SelectionActionBarProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showVerifyDialog, setShowVerifyDialog] = useState(false);
const allSelected = selectedCount === totalCount && totalCount > 0; const allSelected = selectedCount === totalCount && totalCount > 0;
@@ -38,7 +45,16 @@ export function SelectionActionBar({
const handleConfirmDelete = () => { const handleConfirmDelete = () => {
setShowDeleteDialog(false); setShowDeleteDialog(false);
onDelete(); onDelete?.();
};
const handleVerifyClick = () => {
setShowVerifyDialog(true);
};
const handleConfirmVerify = () => {
setShowVerifyDialog(false);
onVerify?.();
}; };
return ( return (
@@ -54,36 +70,56 @@ export function SelectionActionBar({
> >
<span className="text-sm font-medium text-foreground"> <span className="text-sm font-medium text-foreground">
{selectedCount === 0 {selectedCount === 0
? 'Select features to edit' ? mode === 'waiting_approval'
? 'Select features to verify'
: 'Select features to edit'
: `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`} : `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
</span> </span>
<div className="h-4 w-px bg-border" /> <div className="h-4 w-px bg-border" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button {mode === 'backlog' && (
variant="default" <>
size="sm" <Button
onClick={onEdit} variant="default"
disabled={selectedCount === 0} size="sm"
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50" onClick={onEdit}
data-testid="selection-edit-button" disabled={selectedCount === 0}
> className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
<Pencil className="w-4 h-4 mr-1.5" /> data-testid="selection-edit-button"
Edit Selected >
</Button> <Pencil className="w-4 h-4 mr-1.5" />
Edit Selected
</Button>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleDeleteClick} onClick={handleDeleteClick}
disabled={selectedCount === 0} disabled={selectedCount === 0}
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50" className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
data-testid="selection-delete-button" data-testid="selection-delete-button"
> >
<Trash2 className="w-4 h-4 mr-1.5" /> <Trash2 className="w-4 h-4 mr-1.5" />
Delete Delete
</Button> </Button>
</>
)}
{mode === 'waiting_approval' && (
<Button
variant="default"
size="sm"
onClick={handleVerifyClick}
disabled={selectedCount === 0}
className="h-8 bg-green-600 hover:bg-green-700 disabled:opacity-50"
data-testid="selection-verify-button"
>
<CheckCircle2 className="w-4 h-4 mr-1.5" />
Verify Selected
</Button>
)}
{!allSelected && ( {!allSelected && (
<Button <Button
@@ -146,6 +182,42 @@ export function SelectionActionBar({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Verify Confirmation Dialog */}
<Dialog open={showVerifyDialog} onOpenChange={setShowVerifyDialog}>
<DialogContent data-testid="bulk-verify-confirmation-dialog">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-green-600">
<CheckCircle2 className="w-5 h-5" />
Verify Selected Features?
</DialogTitle>
<DialogDescription>
Are you sure you want to mark {selectedCount} feature
{selectedCount !== 1 ? 's' : ''} as verified?
<span className="block mt-2 text-muted-foreground">
This will move them to the Verified column.
</span>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowVerifyDialog(false)}
data-testid="cancel-bulk-verify-button"
>
Cancel
</Button>
<Button
className="bg-green-600 hover:bg-green-700"
onClick={handleConfirmVerify}
data-testid="confirm-bulk-verify-button"
>
<CheckCircle2 className="w-4 h-4 mr-2" />
Verify
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</> </>
); );
} }

View File

@@ -7,5 +7,5 @@ export { useBoardEffects } from './use-board-effects';
export { useBoardBackground } from './use-board-background'; export { useBoardBackground } from './use-board-background';
export { useBoardPersistence } from './use-board-persistence'; export { useBoardPersistence } from './use-board-persistence';
export { useFollowUpState } from './use-follow-up-state'; export { useFollowUpState } from './use-follow-up-state';
export { useSelectionMode } from './use-selection-mode'; export { useSelectionMode, type SelectionTarget } from './use-selection-mode';
export { useListViewState } from './use-list-view-state'; export { useListViewState } from './use-list-view-state';

View File

@@ -1,10 +1,13 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
export type SelectionTarget = 'backlog' | 'waiting_approval' | null;
interface UseSelectionModeReturn { interface UseSelectionModeReturn {
isSelectionMode: boolean; isSelectionMode: boolean;
selectionTarget: SelectionTarget;
selectedFeatureIds: Set<string>; selectedFeatureIds: Set<string>;
selectedCount: number; selectedCount: number;
toggleSelectionMode: () => void; toggleSelectionMode: (target?: SelectionTarget) => void;
toggleFeatureSelection: (featureId: string) => void; toggleFeatureSelection: (featureId: string) => void;
selectAll: (featureIds: string[]) => void; selectAll: (featureIds: string[]) => void;
clearSelection: () => void; clearSelection: () => void;
@@ -13,21 +16,26 @@ interface UseSelectionModeReturn {
} }
export function useSelectionMode(): UseSelectionModeReturn { export function useSelectionMode(): UseSelectionModeReturn {
const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectionTarget, setSelectionTarget] = useState<SelectionTarget>(null);
const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set()); const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set());
const toggleSelectionMode = useCallback(() => { const isSelectionMode = selectionTarget !== null;
setIsSelectionMode((prev) => {
if (prev) { const toggleSelectionMode = useCallback((target: SelectionTarget = 'backlog') => {
setSelectionTarget((prev) => {
if (prev === target) {
// Exiting selection mode - clear selection // Exiting selection mode - clear selection
setSelectedFeatureIds(new Set()); setSelectedFeatureIds(new Set());
return null;
} }
return !prev; // Switching to a different target or entering selection mode
setSelectedFeatureIds(new Set());
return target;
}); });
}, []); }, []);
const exitSelectionMode = useCallback(() => { const exitSelectionMode = useCallback(() => {
setIsSelectionMode(false); setSelectionTarget(null);
setSelectedFeatureIds(new Set()); setSelectedFeatureIds(new Set());
}, []); }, []);
@@ -70,6 +78,7 @@ export function useSelectionMode(): UseSelectionModeReturn {
return { return {
isSelectionMode, isSelectionMode,
selectionTarget,
selectedFeatureIds, selectedFeatureIds,
selectedCount: selectedFeatureIds.size, selectedCount: selectedFeatureIds.size,
toggleSelectionMode, toggleSelectionMode,

View File

@@ -50,9 +50,10 @@ interface KanbanBoardProps {
onOpenPipelineSettings?: () => void; onOpenPipelineSettings?: () => void;
// Selection mode props // Selection mode props
isSelectionMode?: boolean; isSelectionMode?: boolean;
selectionTarget?: 'backlog' | 'waiting_approval' | null;
selectedFeatureIds?: Set<string>; selectedFeatureIds?: Set<string>;
onToggleFeatureSelection?: (featureId: string) => void; onToggleFeatureSelection?: (featureId: string) => void;
onToggleSelectionMode?: () => void; onToggleSelectionMode?: (target?: 'backlog' | 'waiting_approval') => void;
// Empty state action props // Empty state action props
onAiSuggest?: () => void; onAiSuggest?: () => void;
/** Whether currently dragging (hides empty states during drag) */ /** Whether currently dragging (hides empty states during drag) */
@@ -95,6 +96,7 @@ export function KanbanBoard({
pipelineConfig, pipelineConfig,
onOpenPipelineSettings, onOpenPipelineSettings,
isSelectionMode = false, isSelectionMode = false,
selectionTarget = null,
selectedFeatureIds = new Set(), selectedFeatureIds = new Set(),
onToggleFeatureSelection, onToggleFeatureSelection,
onToggleSelectionMode, onToggleSelectionMode,
@@ -189,12 +191,14 @@ export function KanbanBoard({
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`} className={`h-6 px-2 text-xs ${selectionTarget === 'backlog' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
onClick={onToggleSelectionMode} onClick={() => onToggleSelectionMode?.('backlog')}
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'} title={
selectionTarget === 'backlog' ? 'Switch to Drag Mode' : 'Select Multiple'
}
data-testid="selection-mode-button" data-testid="selection-mode-button"
> >
{isSelectionMode ? ( {selectionTarget === 'backlog' ? (
<> <>
<GripVertical className="w-3.5 h-3.5 mr-1" /> <GripVertical className="w-3.5 h-3.5 mr-1" />
Drag Drag
@@ -207,6 +211,31 @@ export function KanbanBoard({
)} )}
</Button> </Button>
</div> </div>
) : column.id === 'waiting_approval' ? (
<Button
variant="ghost"
size="sm"
className={`h-6 px-2 text-xs ${selectionTarget === 'waiting_approval' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
onClick={() => onToggleSelectionMode?.('waiting_approval')}
title={
selectionTarget === 'waiting_approval'
? 'Switch to Drag Mode'
: 'Select Multiple'
}
data-testid="waiting-approval-selection-mode-button"
>
{selectionTarget === 'waiting_approval' ? (
<>
<GripVertical className="w-3.5 h-3.5 mr-1" />
Drag
</>
) : (
<>
<CheckSquare className="w-3.5 h-3.5 mr-1" />
Select
</>
)}
</Button>
) : column.id === 'in_progress' ? ( ) : column.id === 'in_progress' ? (
<Button <Button
variant="ghost" variant="ghost"
@@ -305,6 +334,7 @@ export function KanbanBoard({
cardBorderEnabled={backgroundSettings.cardBorderEnabled} cardBorderEnabled={backgroundSettings.cardBorderEnabled}
cardBorderOpacity={backgroundSettings.cardBorderOpacity} cardBorderOpacity={backgroundSettings.cardBorderOpacity}
isSelectionMode={isSelectionMode} isSelectionMode={isSelectionMode}
selectionTarget={selectionTarget}
isSelected={selectedFeatureIds.has(feature.id)} isSelected={selectedFeatureIds.has(feature.id)}
onToggleSelect={() => onToggleFeatureSelection?.(feature.id)} onToggleSelect={() => onToggleFeatureSelection?.(feature.id)}
/> />

View File

@@ -43,11 +43,11 @@ export const CLAUDE_MODELS: ModelOption[] = [
/** /**
* Cursor models derived from CURSOR_MODEL_MAP * Cursor models derived from CURSOR_MODEL_MAP
* ID is prefixed with "cursor-" for ProviderFactory routing * ID is prefixed with "cursor-" for ProviderFactory routing (if not already prefixed)
*/ */
export const CURSOR_MODELS: ModelOption[] = Object.entries(CURSOR_MODEL_MAP).map( export const CURSOR_MODELS: ModelOption[] = Object.entries(CURSOR_MODEL_MAP).map(
([id, config]) => ({ ([id, config]) => ({
id: `cursor-${id}`, id: id.startsWith('cursor-') ? id : `cursor-${id}`,
label: config.label, label: config.label,
description: config.description, description: config.description,
provider: 'cursor' as ModelProvider, provider: 'cursor' as ModelProvider,

View File

@@ -22,6 +22,10 @@ export type CursorModelId =
| 'cursor-gpt-5.1-codex-high' // GPT-5.1 Codex High via Cursor | 'cursor-gpt-5.1-codex-high' // GPT-5.1 Codex High via Cursor
| 'cursor-gpt-5.1-codex-max' // GPT-5.1 Codex Max via Cursor | 'cursor-gpt-5.1-codex-max' // GPT-5.1 Codex Max via Cursor
| 'cursor-gpt-5.1-codex-max-high' // GPT-5.1 Codex Max High via Cursor | 'cursor-gpt-5.1-codex-max-high' // GPT-5.1 Codex Max High via Cursor
| 'cursor-gpt-5.2-codex' // GPT-5.2 Codex via Cursor
| 'cursor-gpt-5.2-codex-high' // GPT-5.2 Codex High via Cursor
| 'cursor-gpt-5.2-codex-max' // GPT-5.2 Codex Max via Cursor
| 'cursor-gpt-5.2-codex-max-high' // GPT-5.2 Codex Max High via Cursor
| 'grok'; // Grok | 'grok'; // Grok
/** /**
@@ -159,6 +163,34 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
hasThinking: false, hasThinking: false,
supportsVision: false, supportsVision: false,
}, },
'cursor-gpt-5.2-codex': {
id: 'cursor-gpt-5.2-codex',
label: 'GPT-5.2 Codex',
description: 'OpenAI GPT-5.2 Codex for code generation',
hasThinking: false,
supportsVision: false,
},
'cursor-gpt-5.2-codex-high': {
id: 'cursor-gpt-5.2-codex-high',
label: 'GPT-5.2 Codex High',
description: 'OpenAI GPT-5.2 Codex with high compute',
hasThinking: false,
supportsVision: false,
},
'cursor-gpt-5.2-codex-max': {
id: 'cursor-gpt-5.2-codex-max',
label: 'GPT-5.2 Codex Max',
description: 'OpenAI GPT-5.2 Codex Max capacity',
hasThinking: false,
supportsVision: false,
},
'cursor-gpt-5.2-codex-max-high': {
id: 'cursor-gpt-5.2-codex-max-high',
label: 'GPT-5.2 Codex Max High',
description: 'OpenAI GPT-5.2 Codex Max with high compute',
hasThinking: false,
supportsVision: false,
},
grok: { grok: {
id: 'grok', id: 'grok',
label: 'Grok', label: 'Grok',
@@ -284,6 +316,34 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
}, },
], ],
}, },
// GPT-5.2 Codex group (capacity + compute matrix)
{
baseId: 'cursor-gpt-5.2-codex-group',
label: 'GPT-5.2 Codex',
description: 'OpenAI GPT-5.2 Codex for code generation',
variantType: 'capacity',
variants: [
{ id: 'cursor-gpt-5.2-codex', label: 'Standard', description: 'Default capacity' },
{
id: 'cursor-gpt-5.2-codex-high',
label: 'High',
description: 'High compute',
badge: 'Compute',
},
{
id: 'cursor-gpt-5.2-codex-max',
label: 'Max',
description: 'Maximum capacity',
badge: 'Capacity',
},
{
id: 'cursor-gpt-5.2-codex-max-high',
label: 'Max High',
description: 'Max capacity + high compute',
badge: 'Premium',
},
],
},
// Sonnet 4.5 group (thinking mode) // Sonnet 4.5 group (thinking mode)
{ {
baseId: 'sonnet-4.5-group', baseId: 'sonnet-4.5-group',