mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -30,19 +30,27 @@ export function createBulkDeleteHandler(featureLoader: FeatureLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
featureIds.map(async (featureId) => {
|
||||
const success = await featureLoader.delete(projectPath, featureId);
|
||||
if (success) {
|
||||
return { featureId, success: true };
|
||||
}
|
||||
return {
|
||||
featureId,
|
||||
success: false,
|
||||
error: 'Deletion failed. Check server logs for details.',
|
||||
};
|
||||
})
|
||||
);
|
||||
// Process in parallel batches of 20 for efficiency
|
||||
const BATCH_SIZE = 20;
|
||||
const results: BulkDeleteResult[] = [];
|
||||
|
||||
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
|
||||
const batch = featureIds.slice(i, i + BATCH_SIZE);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (featureId) => {
|
||||
const success = await featureLoader.delete(projectPath, featureId);
|
||||
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 failureCount = results.length - successCount;
|
||||
|
||||
@@ -43,17 +43,36 @@ export function createBulkUpdateHandler(featureLoader: FeatureLoader) {
|
||||
const results: BulkUpdateResult[] = [];
|
||||
const updatedFeatures: Feature[] = [];
|
||||
|
||||
for (const featureId of featureIds) {
|
||||
try {
|
||||
const updated = await featureLoader.update(projectPath, featureId, updates);
|
||||
results.push({ featureId, success: true });
|
||||
updatedFeatures.push(updated);
|
||||
} catch (error) {
|
||||
results.push({
|
||||
featureId,
|
||||
success: false,
|
||||
error: getErrorMessage(error),
|
||||
});
|
||||
// Process in parallel batches of 20 for efficiency
|
||||
const BATCH_SIZE = 20;
|
||||
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
|
||||
const batch = featureIds.slice(i, i + BATCH_SIZE);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (featureId) => {
|
||||
try {
|
||||
const updated = await featureLoader.update(projectPath, featureId, updates);
|
||||
return { featureId, success: true as const, feature: updated };
|
||||
} 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ export function BoardView() {
|
||||
// Selection mode hook for mass editing
|
||||
const {
|
||||
isSelectionMode,
|
||||
selectionTarget,
|
||||
selectedFeatureIds,
|
||||
selectedCount,
|
||||
toggleSelectionMode,
|
||||
@@ -684,6 +685,67 @@ export function BoardView() {
|
||||
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
|
||||
const handleAddressPRComments = useCallback(
|
||||
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
||||
@@ -1448,6 +1510,7 @@ export function BoardView() {
|
||||
pipelineConfig={pipelineConfig}
|
||||
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
selectionTarget={selectionTarget}
|
||||
selectedFeatureIds={selectedFeatureIds}
|
||||
onToggleFeatureSelection={toggleFeatureSelection}
|
||||
onToggleSelectionMode={toggleSelectionMode}
|
||||
@@ -1463,11 +1526,23 @@ export function BoardView() {
|
||||
{isSelectionMode && (
|
||||
<SelectionActionBar
|
||||
selectedCount={selectedCount}
|
||||
totalCount={allSelectableFeatureIds.length}
|
||||
onEdit={() => setShowMassEditDialog(true)}
|
||||
onDelete={handleBulkDelete}
|
||||
totalCount={
|
||||
selectionTarget === 'waiting_approval'
|
||||
? allSelectableWaitingApprovalFeatureIds.length
|
||||
: allSelectableFeatureIds.length
|
||||
}
|
||||
onEdit={selectionTarget === 'backlog' ? () => setShowMassEditDialog(true) : undefined}
|
||||
onDelete={selectionTarget === 'backlog' ? handleBulkDelete : undefined}
|
||||
onVerify={selectionTarget === 'waiting_approval' ? handleBulkVerify : undefined}
|
||||
onClear={clearSelection}
|
||||
onSelectAll={() => selectAll(allSelectableFeatureIds)}
|
||||
onSelectAll={() =>
|
||||
selectAll(
|
||||
selectionTarget === 'waiting_approval'
|
||||
? allSelectableWaitingApprovalFeatureIds
|
||||
: allSelectableFeatureIds
|
||||
)
|
||||
}
|
||||
mode={selectionTarget === 'waiting_approval' ? 'waiting_approval' : 'backlog'}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ interface KanbanCardProps {
|
||||
isSelectionMode?: boolean;
|
||||
isSelected?: boolean;
|
||||
onToggleSelect?: () => void;
|
||||
selectionTarget?: 'backlog' | 'waiting_approval' | null;
|
||||
}
|
||||
|
||||
export const KanbanCard = memo(function KanbanCard({
|
||||
@@ -96,6 +97,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
isSelectionMode = false,
|
||||
isSelected = false,
|
||||
onToggleSelect,
|
||||
selectionTarget = null,
|
||||
}: KanbanCardProps) {
|
||||
const { useWorktrees } = useAppStore();
|
||||
const [isLifted, setIsLifted] = useState(false);
|
||||
@@ -125,8 +127,8 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
|
||||
const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity);
|
||||
|
||||
// Only allow selection for backlog features
|
||||
const isSelectable = isSelectionMode && feature.status === 'backlog';
|
||||
// Only allow selection for features matching the selection target
|
||||
const isSelectable = isSelectionMode && feature.status === selectionTarget;
|
||||
|
||||
const wrapperClasses = cn(
|
||||
'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 */}
|
||||
<div className="px-3 pt-3 flex items-center gap-2">
|
||||
{isSelectionMode && !isOverlay && feature.status === 'backlog' && (
|
||||
{isSelectable && !isOverlay && (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggleSelect?.()}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
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 {
|
||||
Dialog,
|
||||
@@ -11,13 +11,17 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
export type SelectionActionMode = 'backlog' | 'waiting_approval';
|
||||
|
||||
interface SelectionActionBarProps {
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onVerify?: () => void;
|
||||
onClear: () => void;
|
||||
onSelectAll: () => void;
|
||||
mode?: SelectionActionMode;
|
||||
}
|
||||
|
||||
export function SelectionActionBar({
|
||||
@@ -25,10 +29,13 @@ export function SelectionActionBar({
|
||||
totalCount,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onVerify,
|
||||
onClear,
|
||||
onSelectAll,
|
||||
mode = 'backlog',
|
||||
}: SelectionActionBarProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showVerifyDialog, setShowVerifyDialog] = useState(false);
|
||||
|
||||
const allSelected = selectedCount === totalCount && totalCount > 0;
|
||||
|
||||
@@ -38,7 +45,16 @@ export function SelectionActionBar({
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
setShowDeleteDialog(false);
|
||||
onDelete();
|
||||
onDelete?.();
|
||||
};
|
||||
|
||||
const handleVerifyClick = () => {
|
||||
setShowVerifyDialog(true);
|
||||
};
|
||||
|
||||
const handleConfirmVerify = () => {
|
||||
setShowVerifyDialog(false);
|
||||
onVerify?.();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -54,36 +70,56 @@ export function SelectionActionBar({
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{selectedCount === 0
|
||||
? 'Select features to edit'
|
||||
? mode === 'waiting_approval'
|
||||
? 'Select features to verify'
|
||||
: 'Select features to edit'
|
||||
: `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
|
||||
</span>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
|
||||
data-testid="selection-edit-button"
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-1.5" />
|
||||
Edit Selected
|
||||
</Button>
|
||||
{mode === 'backlog' && (
|
||||
<>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
|
||||
data-testid="selection-edit-button"
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-1.5" />
|
||||
Edit Selected
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||
data-testid="selection-delete-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1.5" />
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||
data-testid="selection-delete-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1.5" />
|
||||
Delete
|
||||
</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 && (
|
||||
<Button
|
||||
@@ -146,6 +182,42 @@ export function SelectionActionBar({
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ export { useBoardEffects } from './use-board-effects';
|
||||
export { useBoardBackground } from './use-board-background';
|
||||
export { useBoardPersistence } from './use-board-persistence';
|
||||
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';
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
export type SelectionTarget = 'backlog' | 'waiting_approval' | null;
|
||||
|
||||
interface UseSelectionModeReturn {
|
||||
isSelectionMode: boolean;
|
||||
selectionTarget: SelectionTarget;
|
||||
selectedFeatureIds: Set<string>;
|
||||
selectedCount: number;
|
||||
toggleSelectionMode: () => void;
|
||||
toggleSelectionMode: (target?: SelectionTarget) => void;
|
||||
toggleFeatureSelection: (featureId: string) => void;
|
||||
selectAll: (featureIds: string[]) => void;
|
||||
clearSelection: () => void;
|
||||
@@ -13,21 +16,26 @@ interface 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 toggleSelectionMode = useCallback(() => {
|
||||
setIsSelectionMode((prev) => {
|
||||
if (prev) {
|
||||
const isSelectionMode = selectionTarget !== null;
|
||||
|
||||
const toggleSelectionMode = useCallback((target: SelectionTarget = 'backlog') => {
|
||||
setSelectionTarget((prev) => {
|
||||
if (prev === target) {
|
||||
// Exiting selection mode - clear selection
|
||||
setSelectedFeatureIds(new Set());
|
||||
return null;
|
||||
}
|
||||
return !prev;
|
||||
// Switching to a different target or entering selection mode
|
||||
setSelectedFeatureIds(new Set());
|
||||
return target;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const exitSelectionMode = useCallback(() => {
|
||||
setIsSelectionMode(false);
|
||||
setSelectionTarget(null);
|
||||
setSelectedFeatureIds(new Set());
|
||||
}, []);
|
||||
|
||||
@@ -70,6 +78,7 @@ export function useSelectionMode(): UseSelectionModeReturn {
|
||||
|
||||
return {
|
||||
isSelectionMode,
|
||||
selectionTarget,
|
||||
selectedFeatureIds,
|
||||
selectedCount: selectedFeatureIds.size,
|
||||
toggleSelectionMode,
|
||||
|
||||
@@ -50,9 +50,10 @@ interface KanbanBoardProps {
|
||||
onOpenPipelineSettings?: () => void;
|
||||
// Selection mode props
|
||||
isSelectionMode?: boolean;
|
||||
selectionTarget?: 'backlog' | 'waiting_approval' | null;
|
||||
selectedFeatureIds?: Set<string>;
|
||||
onToggleFeatureSelection?: (featureId: string) => void;
|
||||
onToggleSelectionMode?: () => void;
|
||||
onToggleSelectionMode?: (target?: 'backlog' | 'waiting_approval') => void;
|
||||
// Empty state action props
|
||||
onAiSuggest?: () => void;
|
||||
/** Whether currently dragging (hides empty states during drag) */
|
||||
@@ -95,6 +96,7 @@ export function KanbanBoard({
|
||||
pipelineConfig,
|
||||
onOpenPipelineSettings,
|
||||
isSelectionMode = false,
|
||||
selectionTarget = null,
|
||||
selectedFeatureIds = new Set(),
|
||||
onToggleFeatureSelection,
|
||||
onToggleSelectionMode,
|
||||
@@ -189,12 +191,14 @@ export function KanbanBoard({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={onToggleSelectionMode}
|
||||
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
|
||||
className={`h-6 px-2 text-xs ${selectionTarget === 'backlog' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||
onClick={() => onToggleSelectionMode?.('backlog')}
|
||||
title={
|
||||
selectionTarget === 'backlog' ? 'Switch to Drag Mode' : 'Select Multiple'
|
||||
}
|
||||
data-testid="selection-mode-button"
|
||||
>
|
||||
{isSelectionMode ? (
|
||||
{selectionTarget === 'backlog' ? (
|
||||
<>
|
||||
<GripVertical className="w-3.5 h-3.5 mr-1" />
|
||||
Drag
|
||||
@@ -207,6 +211,31 @@ export function KanbanBoard({
|
||||
)}
|
||||
</Button>
|
||||
</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' ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -305,6 +334,7 @@ export function KanbanBoard({
|
||||
cardBorderEnabled={backgroundSettings.cardBorderEnabled}
|
||||
cardBorderOpacity={backgroundSettings.cardBorderOpacity}
|
||||
isSelectionMode={isSelectionMode}
|
||||
selectionTarget={selectionTarget}
|
||||
isSelected={selectedFeatureIds.has(feature.id)}
|
||||
onToggleSelect={() => onToggleFeatureSelection?.(feature.id)}
|
||||
/>
|
||||
|
||||
@@ -43,11 +43,11 @@ export const CLAUDE_MODELS: ModelOption[] = [
|
||||
|
||||
/**
|
||||
* 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(
|
||||
([id, config]) => ({
|
||||
id: `cursor-${id}`,
|
||||
id: id.startsWith('cursor-') ? id : `cursor-${id}`,
|
||||
label: config.label,
|
||||
description: config.description,
|
||||
provider: 'cursor' as ModelProvider,
|
||||
|
||||
@@ -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-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.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
|
||||
|
||||
/**
|
||||
@@ -159,6 +163,34 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
|
||||
hasThinking: 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: {
|
||||
id: '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)
|
||||
{
|
||||
baseId: 'sonnet-4.5-group',
|
||||
|
||||
Reference in New Issue
Block a user