feat(ui): Enhance AI model handling with Cursor support

- Refactor model handling to support both Claude and Cursor models across various components.
- Introduce `stripProviderPrefix` utility for consistent model ID processing.
- Update `CursorProvider` to utilize `isCursorModel` for model validation.
- Implement model override functionality in GitHub issue validation and enhancement routes.
- Add `useCursorStatusInit` hook to initialize Cursor CLI status on app startup.
- Update UI components to reflect changes in model selection and validation processes.

This update improves the flexibility of AI model usage and enhances user experience by allowing quick model overrides.
This commit is contained in:
Kacper
2025-12-30 04:01:56 +01:00
parent 3d655c3298
commit 39f2c8c9ff
38 changed files with 713 additions and 258 deletions

View File

@@ -4,14 +4,15 @@ import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useAppStore } from '@/store/app-store';
import type { AgentModel, CursorModelId, PhaseModelKey } from '@automaker/types';
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
import { PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
export interface ModelOverrideTriggerProps {
/** Current effective model (from global settings or explicit override) */
currentModel: AgentModel | CursorModelId;
currentModel: ModelAlias | CursorModelId;
/** Callback when user selects override */
onModelChange: (model: AgentModel | CursorModelId | null) => void;
onModelChange: (model: ModelAlias | CursorModelId | null) => void;
/** Optional: which phase this is for (shows global default) */
phase?: PhaseModelKey;
/** Size variants for different contexts */
@@ -24,13 +25,13 @@ export interface ModelOverrideTriggerProps {
className?: string;
}
function getModelLabel(modelId: AgentModel | CursorModelId): string {
function getModelLabel(modelId: ModelAlias | CursorModelId): string {
// Check Claude models
const claudeModel = CLAUDE_MODELS.find((m) => m.id === modelId);
if (claudeModel) return claudeModel.label;
// Check Cursor models (without cursor- prefix)
const cursorModel = CURSOR_MODELS.find((m) => m.id === `cursor-${modelId}`);
const cursorModel = CURSOR_MODELS.find((m) => m.id === `${PROVIDER_PREFIXES.cursor}${modelId}`);
if (cursorModel) return cursorModel.label;
// Check Cursor models (with cursor- prefix)
@@ -57,11 +58,11 @@ export function ModelOverrideTrigger({
// Filter Cursor models to only show enabled ones
const availableCursorModels = CURSOR_MODELS.filter((model) => {
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
return enabledCursorModels.includes(cursorId);
});
const handleSelect = (model: AgentModel | CursorModelId) => {
const handleSelect = (model: ModelAlias | CursorModelId) => {
onModelChange(model);
setOpen(false);
};
@@ -162,7 +163,7 @@ export function ModelOverrideTrigger({
return (
<button
key={model.id}
onClick={() => handleSelect(model.id as AgentModel)}
onClick={() => handleSelect(model.id as ModelAlias)}
className={cn(
'px-3 py-2 rounded-lg text-xs font-medium text-center',
'transition-all duration-150',
@@ -190,7 +191,7 @@ export function ModelOverrideTrigger({
</h5>
<div className="grid grid-cols-2 gap-2">
{availableCursorModels.slice(0, 6).map((model) => {
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
const isActive = currentModel === cursorId;
return (
<button

View File

@@ -1,27 +1,27 @@
import { useState, useCallback, useMemo } from 'react';
import { useAppStore } from '@/store/app-store';
import type { AgentModel, CursorModelId, PhaseModelKey } from '@automaker/types';
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
export interface UseModelOverrideOptions {
/** Which phase this override is for */
phase: PhaseModelKey;
/** Initial override value (optional) */
initialOverride?: AgentModel | CursorModelId | null;
initialOverride?: ModelAlias | CursorModelId | null;
}
export interface UseModelOverrideResult {
/** The effective model (override or global default) */
effectiveModel: AgentModel | CursorModelId;
effectiveModel: ModelAlias | CursorModelId;
/** Whether the model is currently overridden */
isOverridden: boolean;
/** Set a model override */
setOverride: (model: AgentModel | CursorModelId | null) => void;
setOverride: (model: ModelAlias | CursorModelId | null) => void;
/** Clear the override and use global default */
clearOverride: () => void;
/** The global default for this phase */
globalDefault: AgentModel | CursorModelId;
globalDefault: ModelAlias | CursorModelId;
/** The current override value (null if not overridden) */
override: AgentModel | CursorModelId | null;
override: ModelAlias | CursorModelId | null;
}
/**
@@ -53,7 +53,7 @@ export function useModelOverride({
initialOverride = null,
}: UseModelOverrideOptions): UseModelOverrideResult {
const { phaseModels } = useAppStore();
const [override, setOverrideState] = useState<AgentModel | CursorModelId | null>(initialOverride);
const [override, setOverrideState] = useState<ModelAlias | CursorModelId | null>(initialOverride);
const globalDefault = phaseModels[phase];
@@ -63,7 +63,7 @@ export function useModelOverride({
const isOverridden = override !== null;
const setOverride = useCallback((model: AgentModel | CursorModelId | null) => {
const setOverride = useCallback((model: ModelAlias | CursorModelId | null) => {
setOverrideState(model);
}, []);