mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -28,6 +28,7 @@ import type {
|
||||
ModelDefinition,
|
||||
ContentBlock,
|
||||
} from './types.js';
|
||||
import { stripProviderPrefix } from '@automaker/types';
|
||||
import {
|
||||
type CursorStreamEvent,
|
||||
type CursorSystemEvent,
|
||||
@@ -115,10 +116,7 @@ export class CursorProvider extends CliProvider {
|
||||
|
||||
buildCliArgs(options: ExecuteOptions): string[] {
|
||||
// Extract model (strip 'cursor-' prefix if present)
|
||||
let model = options.model || 'auto';
|
||||
if (model.startsWith('cursor-')) {
|
||||
model = model.substring(7);
|
||||
}
|
||||
const model = stripProviderPrefix(options.model || 'auto');
|
||||
|
||||
// Build prompt content
|
||||
let promptText: string;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import type { InstallationStatus, ModelDefinition } from './types.js';
|
||||
import { CURSOR_MODEL_MAP, type ModelProvider } from '@automaker/types';
|
||||
import { isCursorModel, type ModelProvider } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Provider registration entry
|
||||
@@ -181,14 +181,6 @@ registerProvider('claude', {
|
||||
// Register Cursor provider
|
||||
registerProvider('cursor', {
|
||||
factory: () => new CursorProvider(),
|
||||
canHandleModel: (model: string) => {
|
||||
// Check for explicit cursor prefix
|
||||
if (model.startsWith('cursor-')) {
|
||||
return true;
|
||||
}
|
||||
// Check if it's a known Cursor model ID
|
||||
const modelId = model.replace('cursor-', '');
|
||||
return modelId in CURSOR_MODEL_MAP;
|
||||
},
|
||||
canHandleModel: (model: string) => isCursorModel(model),
|
||||
priority: 10, // Higher priority - check Cursor models first
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* POST /enhance-prompt endpoint - Enhance user input text
|
||||
*
|
||||
* Uses Claude AI to enhance text based on the specified enhancement mode.
|
||||
* Uses Claude AI or Cursor to enhance text based on the specified enhancement mode.
|
||||
* Supports modes: improve, technical, simplify, acceptance
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,8 @@ import type { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||
import { CLAUDE_MODEL_MAP, isCursorModel } from '@automaker/types';
|
||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||
import {
|
||||
getSystemPrompt,
|
||||
buildUserPrompt,
|
||||
@@ -80,6 +81,40 @@ async function extractTextFromStream(
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute enhancement using Cursor provider
|
||||
*
|
||||
* @param prompt - The enhancement prompt
|
||||
* @param model - The Cursor model to use
|
||||
* @returns The enhanced text
|
||||
*/
|
||||
async function executeWithCursor(prompt: string, model: string): Promise<string> {
|
||||
const provider = ProviderFactory.getProviderForModel(model);
|
||||
|
||||
let responseText = '';
|
||||
|
||||
for await (const msg of provider.executeQuery({
|
||||
prompt,
|
||||
model,
|
||||
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
|
||||
})) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
responseText += block.text;
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||
// Use result if it's a final accumulated message
|
||||
if (msg.result.length > responseText.length) {
|
||||
responseText = msg.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the enhance request handler
|
||||
*
|
||||
@@ -140,24 +175,36 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
|
||||
|
||||
logger.debug(`Using model: ${resolvedModel}`);
|
||||
|
||||
// Call Claude SDK with minimal configuration for text transformation
|
||||
// Key: no tools, just text completion
|
||||
const stream = query({
|
||||
prompt: userPrompt,
|
||||
options: {
|
||||
model: resolvedModel,
|
||||
systemPrompt,
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
permissionMode: 'acceptEdits',
|
||||
},
|
||||
});
|
||||
let enhancedText: string;
|
||||
|
||||
// Extract the enhanced text from the response
|
||||
const enhancedText = await extractTextFromStream(stream);
|
||||
// Route to appropriate provider based on model
|
||||
if (isCursorModel(resolvedModel)) {
|
||||
// Use Cursor provider for Cursor models
|
||||
logger.info(`Using Cursor provider for model: ${resolvedModel}`);
|
||||
|
||||
// Cursor doesn't have a separate system prompt concept, so combine them
|
||||
const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
||||
enhancedText = await executeWithCursor(combinedPrompt, resolvedModel);
|
||||
} else {
|
||||
// Use Claude SDK for Claude models
|
||||
logger.info(`Using Claude provider for model: ${resolvedModel}`);
|
||||
|
||||
const stream = query({
|
||||
prompt: userPrompt,
|
||||
options: {
|
||||
model: resolvedModel,
|
||||
systemPrompt,
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
permissionMode: 'acceptEdits',
|
||||
},
|
||||
});
|
||||
|
||||
enhancedText = await extractTextFromStream(stream);
|
||||
}
|
||||
|
||||
if (!enhancedText || enhancedText.trim().length === 0) {
|
||||
logger.warn('Received empty response from Claude');
|
||||
logger.warn('Received empty response from AI');
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: 'Failed to generate enhanced text - empty response',
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
/**
|
||||
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK (async)
|
||||
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK or Cursor (async)
|
||||
*
|
||||
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
|
||||
* Runs asynchronously and emits events for progress and completion.
|
||||
* Supports both Claude models and Cursor models.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import type { IssueValidationResult, IssueValidationEvent, AgentModel } from '@automaker/types';
|
||||
import type {
|
||||
IssueValidationResult,
|
||||
IssueValidationEvent,
|
||||
ModelAlias,
|
||||
CursorModelId,
|
||||
} from '@automaker/types';
|
||||
import { isCursorModel } from '@automaker/types';
|
||||
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||
import {
|
||||
issueValidationSchema,
|
||||
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
@@ -26,8 +34,8 @@ import {
|
||||
import type { SettingsService } from '../../../services/settings-service.js';
|
||||
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
|
||||
|
||||
/** Valid model values for validation */
|
||||
const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
/** Valid Claude model values for validation */
|
||||
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
|
||||
/**
|
||||
* Request body for issue validation
|
||||
@@ -38,8 +46,8 @@ interface ValidateIssueRequestBody {
|
||||
issueTitle: string;
|
||||
issueBody: string;
|
||||
issueLabels?: string[];
|
||||
/** Model to use for validation (opus, sonnet, haiku) */
|
||||
model?: AgentModel;
|
||||
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
|
||||
model?: ModelAlias | CursorModelId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +55,7 @@ interface ValidateIssueRequestBody {
|
||||
*
|
||||
* Emits events for start, progress, complete, and error.
|
||||
* Stores result on completion.
|
||||
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing).
|
||||
*/
|
||||
async function runValidation(
|
||||
projectPath: string,
|
||||
@@ -54,7 +63,7 @@ async function runValidation(
|
||||
issueTitle: string,
|
||||
issueBody: string,
|
||||
issueLabels: string[] | undefined,
|
||||
model: AgentModel,
|
||||
model: ModelAlias | CursorModelId,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController,
|
||||
settingsService?: SettingsService
|
||||
@@ -79,65 +88,133 @@ async function runValidation(
|
||||
// Build the prompt
|
||||
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
|
||||
|
||||
// Load autoLoadClaudeMd setting
|
||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||
projectPath,
|
||||
settingsService,
|
||||
'[ValidateIssue]'
|
||||
);
|
||||
|
||||
// Create SDK options with structured output and abort controller
|
||||
const options = createSuggestionsOptions({
|
||||
cwd: projectPath,
|
||||
model,
|
||||
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
outputFormat: {
|
||||
type: 'json_schema',
|
||||
schema: issueValidationSchema as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute the query
|
||||
const stream = query({ prompt, options });
|
||||
let validationResult: IssueValidationResult | null = null;
|
||||
let responseText = '';
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Collect assistant text for debugging and emit progress
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
// Route to appropriate provider based on model
|
||||
if (isCursorModel(model)) {
|
||||
// Use Cursor provider for Cursor models
|
||||
logger.info(`Using Cursor provider for validation with model: ${model}`);
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
const provider = ProviderFactory.getProviderForModel(model);
|
||||
|
||||
// For Cursor, include the system prompt and schema in the user prompt
|
||||
const cursorPrompt = `${ISSUE_VALIDATION_SYSTEM_PROMPT}
|
||||
|
||||
You MUST respond with a valid JSON object matching this schema:
|
||||
${JSON.stringify(issueValidationSchema, null, 2)}
|
||||
|
||||
${prompt}`;
|
||||
|
||||
for await (const msg of provider.executeQuery({
|
||||
prompt: cursorPrompt,
|
||||
model,
|
||||
cwd: projectPath,
|
||||
})) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
responseText += block.text;
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||
// Use result if it's a final accumulated message
|
||||
if (msg.result.length > responseText.length) {
|
||||
responseText = msg.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract structured output on success
|
||||
if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||
if (resultMsg.structured_output) {
|
||||
validationResult = resultMsg.structured_output;
|
||||
logger.debug('Received structured output:', validationResult);
|
||||
// Parse JSON from the response text
|
||||
if (responseText) {
|
||||
try {
|
||||
// Try to extract JSON from response (it might be wrapped in markdown code blocks)
|
||||
let jsonStr = responseText;
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (jsonMatch) {
|
||||
jsonStr = jsonMatch[1].trim();
|
||||
}
|
||||
|
||||
validationResult = JSON.parse(jsonStr) as IssueValidationResult;
|
||||
logger.debug('Parsed validation result from Cursor response:', validationResult);
|
||||
} catch (parseError) {
|
||||
logger.error('Failed to parse JSON from Cursor response:', parseError);
|
||||
logger.debug('Raw response:', responseText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use Claude SDK for Claude models
|
||||
logger.info(`Using Claude provider for validation with model: ${model}`);
|
||||
|
||||
// Handle errors
|
||||
if (msg.type === 'result') {
|
||||
const resultMsg = msg as { subtype?: string };
|
||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||
logger.error('Failed to produce valid structured output after retries');
|
||||
throw new Error('Could not produce valid validation output');
|
||||
// Load autoLoadClaudeMd setting
|
||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||
projectPath,
|
||||
settingsService,
|
||||
'[ValidateIssue]'
|
||||
);
|
||||
|
||||
// Create SDK options with structured output and abort controller
|
||||
const options = createSuggestionsOptions({
|
||||
cwd: projectPath,
|
||||
model: model as ModelAlias,
|
||||
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
outputFormat: {
|
||||
type: 'json_schema',
|
||||
schema: issueValidationSchema as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute the query
|
||||
const stream = query({ prompt, options });
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Collect assistant text for debugging and emit progress
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract structured output on success
|
||||
if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||
if (resultMsg.structured_output) {
|
||||
validationResult = resultMsg.structured_output;
|
||||
logger.debug('Received structured output:', validationResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if (msg.type === 'result') {
|
||||
const resultMsg = msg as { subtype?: string };
|
||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||
logger.error('Failed to produce valid structured output after retries');
|
||||
throw new Error('Could not produce valid validation output');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,11 +222,11 @@ async function runValidation(
|
||||
// Clear timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Require structured output
|
||||
// Require validation result
|
||||
if (!validationResult) {
|
||||
logger.error('No structured output received from Claude SDK');
|
||||
logger.error('No validation result received from AI provider');
|
||||
logger.debug('Raw response text:', responseText);
|
||||
throw new Error('Validation failed: no structured output received');
|
||||
throw new Error('Validation failed: no valid result received');
|
||||
}
|
||||
|
||||
logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`);
|
||||
@@ -239,11 +316,14 @@ export function createValidateIssueHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate model parameter at runtime
|
||||
if (!VALID_MODELS.includes(model)) {
|
||||
// Validate model parameter at runtime - accept Claude models or Cursor models
|
||||
const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias);
|
||||
const isValidCursorModel = isCursorModel(model);
|
||||
|
||||
if (!isValidClaudeModel && !isValidCursorModel) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid model. Must be one of: ${VALID_MODELS.join(', ')}`,
|
||||
error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export type {
|
||||
ThemeMode,
|
||||
KanbanCardDetailLevel,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouterProvider } from '@tanstack/react-router';
|
||||
import { router } from './utils/router';
|
||||
import { SplashScreen } from './components/splash-screen';
|
||||
import { useSettingsMigration } from './hooks/use-settings-migration';
|
||||
import { useCursorStatusInit } from './hooks/use-cursor-status-init';
|
||||
import './styles/global.css';
|
||||
import './styles/theme-imports';
|
||||
|
||||
@@ -21,6 +22,9 @@ export default function App() {
|
||||
console.log('[App] Settings migrated to file storage');
|
||||
}
|
||||
|
||||
// Initialize Cursor CLI status at startup
|
||||
useCursorStatusInit();
|
||||
|
||||
const handleSplashComplete = useCallback(() => {
|
||||
sessionStorage.setItem('automaker-splash-shown', 'true');
|
||||
setShowSplash(false);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { useAppStore, type AgentModel } from '@/store/app-store';
|
||||
import { useAppStore, type ModelAlias } from '@/store/app-store';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ImageDropZone } from '@/components/ui/image-drop-zone';
|
||||
@@ -63,7 +64,7 @@ export function AgentView() {
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<AgentModel>('sonnet');
|
||||
const [selectedModel, setSelectedModel] = useState<ModelAlias | CursorModelId>('sonnet');
|
||||
|
||||
// Track if initial session has been loaded
|
||||
const initialSessionLoadedRef = useRef(false);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { useAppStore, Feature } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import type { AutoModeEvent } from '@/types/electron';
|
||||
import type { BacklogPlanResult } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, BacklogPlanResult } from '@automaker/types';
|
||||
import { pathsEqual } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
useAppStore,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
PlanningModeSelector,
|
||||
AncestorContextSection,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
formatAncestorContextForPrompt,
|
||||
type AncestorContext,
|
||||
} from '@automaker/dependency-resolver';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
@@ -66,7 +68,7 @@ interface AddFeatureDialogProps {
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string; // Can be empty string to use current branch
|
||||
priority: number;
|
||||
@@ -115,7 +117,7 @@ export function AddFeatureDialog({
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
textFilePaths: [] as DescriptionTextFilePath[],
|
||||
skipTests: false,
|
||||
model: 'opus' as AgentModel,
|
||||
model: 'opus' as ModelAlias,
|
||||
thinkingLevel: 'none' as ThinkingLevel,
|
||||
branchName: '',
|
||||
priority: 2 as number, // Default to medium priority
|
||||
@@ -136,14 +138,12 @@ export function AddFeatureDialog({
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get enhancement model, planning mode defaults, and worktrees setting from store
|
||||
const {
|
||||
enhancementModel,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
useWorktrees,
|
||||
} = useAppStore();
|
||||
// Get planning mode defaults and worktrees setting from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees } =
|
||||
useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
// Sync defaults when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -294,7 +294,7 @@ export function AddFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
newFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -315,11 +315,11 @@ export function AddFeatureDialog({
|
||||
const handleModelSelect = (model: string) => {
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursorModel = model.startsWith('cursor-');
|
||||
const isCursor = isCursorModel(model);
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: model as AgentModel,
|
||||
thinkingLevel: isCursorModel
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? newFeature.thinkingLevel
|
||||
@@ -330,10 +330,10 @@ export function AddFeatureDialog({
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: cursorModel as AgentModel,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
@@ -347,8 +347,9 @@ export function AddFeatureDialog({
|
||||
};
|
||||
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCursorModel = newFeature.model.startsWith('cursor-');
|
||||
const newModelAllowsThinking = !isCursorModel && modelSupportsThinking(newFeature.model);
|
||||
const isCurrentModelCursor = isCursorModel(newFeature.model);
|
||||
const newModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(newFeature.model || 'sonnet');
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||
@@ -480,6 +481,15 @@ export function AddFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
@@ -540,7 +550,7 @@ export function AddFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={newFeature.model}
|
||||
selectedThinkingLevel={newFeature.thinkingLevel}
|
||||
selectedCursorModel={isCursorModel ? newFeature.model : undefined}
|
||||
selectedCursorModel={isCurrentModelCursor ? newFeature.model : undefined}
|
||||
onSelect={handleProfileSelect}
|
||||
showManageLink
|
||||
onManageLinkClick={() => {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
Feature,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
useAppStore,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
BranchSelector,
|
||||
PlanningModeSelector,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface EditFeatureDialogProps {
|
||||
feature: Feature | null;
|
||||
@@ -65,7 +67,7 @@ interface EditFeatureDialogProps {
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
@@ -117,8 +119,11 @@ export function EditFeatureDialog({
|
||||
feature?.requirePlanApproval ?? false
|
||||
);
|
||||
|
||||
// Get enhancement model and worktrees setting from store
|
||||
const { enhancementModel, useWorktrees } = useAppStore();
|
||||
// Get worktrees setting from store
|
||||
const { useWorktrees } = useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
useEffect(() => {
|
||||
setEditingFeature(feature);
|
||||
@@ -148,7 +153,7 @@ export function EditFeatureDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as AgentModel;
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as ModelAlias;
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
|
||||
? (editingFeature.thinkingLevel ?? 'none')
|
||||
: 'none';
|
||||
@@ -187,22 +192,40 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelect = (model: AgentModel) => {
|
||||
const handleModelSelect = (model: string) => {
|
||||
if (!editingFeature) return;
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursor = isCursorModel(model);
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel: modelSupportsThinking(model) ? editingFeature.thinkingLevel : 'none',
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? editingFeature.thinkingLevel
|
||||
: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (!editingFeature) return;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel,
|
||||
});
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
// Claude profile
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnhanceDescription = async () => {
|
||||
@@ -214,7 +237,7 @@ export function EditFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
editingFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -232,7 +255,10 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCurrentModelCursor = isCursorModel(editingFeature?.model as string);
|
||||
const editModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(editingFeature?.model);
|
||||
|
||||
if (!editingFeature) {
|
||||
return null;
|
||||
@@ -361,6 +387,15 @@ export function EditFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-category">Category (optional)</Label>
|
||||
@@ -437,6 +472,9 @@ export function EditFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={editingFeature.model ?? 'opus'}
|
||||
selectedThinkingLevel={editingFeature.thinkingLevel ?? 'none'}
|
||||
selectedCursorModel={
|
||||
isCurrentModelCursor ? (editingFeature.model as string) : undefined
|
||||
}
|
||||
onSelect={handleProfileSelect}
|
||||
testIdPrefix="edit-profile-quick-select"
|
||||
/>
|
||||
@@ -450,7 +488,7 @@ export function EditFeatureDialog({
|
||||
{(!showProfilesOnly || showEditAdvancedOptions) && (
|
||||
<>
|
||||
<ModelSelector
|
||||
selectedModel={(editingFeature.model ?? 'opus') as AgentModel}
|
||||
selectedModel={(editingFeature.model ?? 'opus') as ModelAlias}
|
||||
onModelSelect={handleModelSelect}
|
||||
testIdPrefix="edit-model-select"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
PlanningMode,
|
||||
useAppStore,
|
||||
@@ -92,7 +92,7 @@ export function useBoardActions({
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
@@ -210,7 +210,7 @@ export function useBoardActions({
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
|
||||
export type ModelOption = {
|
||||
id: string; // Claude models use AgentModel, Cursor models use "cursor-{id}"
|
||||
id: string; // Claude models use ModelAlias, Cursor models use "cursor-{id}"
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
|
||||
@@ -2,28 +2,19 @@ import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Brain, Bot, Terminal, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel } from '@/store/app-store';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
selectedModel: string; // Can be AgentModel or "cursor-{id}"
|
||||
selectedModel: string; // Can be ModelAlias or "cursor-{id}"
|
||||
onModelSelect: (model: string) => void;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider from a model string
|
||||
*/
|
||||
function getProviderFromModelString(model: string): ModelProvider {
|
||||
if (model.startsWith('cursor-')) {
|
||||
return 'cursor';
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
selectedModel,
|
||||
onModelSelect,
|
||||
@@ -32,7 +23,7 @@ export function ModelSelector({
|
||||
const { enabledCursorModels, cursorDefaultModel } = useAppStore();
|
||||
const { cursorCliStatus } = useSetupStore();
|
||||
|
||||
const selectedProvider = getProviderFromModelString(selectedModel);
|
||||
const selectedProvider = getModelProvider(selectedModel);
|
||||
|
||||
// Check if Cursor CLI is available
|
||||
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
||||
@@ -40,14 +31,14 @@ export function ModelSelector({
|
||||
// Filter Cursor models based on enabled models from global settings
|
||||
const filteredCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
// Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto")
|
||||
const cursorModelId = model.id.replace('cursor-', '');
|
||||
const cursorModelId = stripProviderPrefix(model.id);
|
||||
return enabledCursorModels.includes(cursorModelId as any);
|
||||
});
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
if (provider === 'cursor' && selectedProvider !== 'cursor') {
|
||||
// Switch to Cursor's default model (from global settings)
|
||||
onModelSelect(`cursor-${cursorDefaultModel}`);
|
||||
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
|
||||
} else if (provider === 'claude' && selectedProvider !== 'claude') {
|
||||
// Switch to Claude's default model
|
||||
onModelSelect('sonnet');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Brain, UserCircle, Terminal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel, ThinkingLevel, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
|
||||
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
import { PROFILE_ICONS } from './model-constants';
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ function getProfileThinkingDisplay(profile: AIProfile): string | null {
|
||||
|
||||
interface ProfileQuickSelectProps {
|
||||
profiles: AIProfile[];
|
||||
selectedModel: AgentModel;
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
selectedThinkingLevel: ThinkingLevel;
|
||||
selectedCursorModel?: string; // For detecting cursor profile selection
|
||||
onSelect: (profile: AIProfile) => void; // Changed to pass full profile
|
||||
@@ -62,7 +62,7 @@ export function ProfileQuickSelect({
|
||||
const isProfileSelected = (profile: AIProfile): boolean => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// For cursor profiles, check if cursor model matches
|
||||
const profileCursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
return selectedCursorModel === profileCursorModel;
|
||||
}
|
||||
// For Claude profiles
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'
|
||||
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
|
||||
import { ValidationDialog } from './github-issues-view/dialogs';
|
||||
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
|
||||
import { useModelOverride } from '@/components/shared';
|
||||
|
||||
export function GitHubIssuesView() {
|
||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||
@@ -21,6 +22,9 @@ export function GitHubIssuesView() {
|
||||
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
||||
useAppStore();
|
||||
|
||||
// Model override for validation
|
||||
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
||||
|
||||
const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues();
|
||||
|
||||
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
|
||||
@@ -85,6 +89,9 @@ export function GitHubIssuesView() {
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
// Use profile default model
|
||||
const featureModel = defaultProfile?.model ?? 'opus';
|
||||
|
||||
const feature = {
|
||||
id: `issue-${issue.number}-${crypto.randomUUID()}`,
|
||||
title: issue.title,
|
||||
@@ -93,7 +100,7 @@ export function GitHubIssuesView() {
|
||||
status: 'backlog' as const,
|
||||
passes: false,
|
||||
priority: getFeaturePriority(validation.estimatedComplexity),
|
||||
model: defaultProfile?.model ?? 'opus',
|
||||
model: featureModel,
|
||||
thinkingLevel: defaultProfile?.thinkingLevel ?? 'none',
|
||||
branchName: currentBranch,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -205,6 +212,7 @@ export function GitHubIssuesView() {
|
||||
onClose={() => setSelectedIssue(null)}
|
||||
onShowRevalidateConfirm={() => setShowRevalidateConfirm(true)}
|
||||
formatDate={formatDate}
|
||||
modelOverride={validationModelOverride}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -228,7 +236,10 @@ export function GitHubIssuesView() {
|
||||
confirmText="Re-validate"
|
||||
onConfirm={() => {
|
||||
if (selectedIssue) {
|
||||
handleValidateIssue(selectedIssue, { forceRevalidate: true });
|
||||
handleValidateIssue(selectedIssue, {
|
||||
forceRevalidate: true,
|
||||
model: validationModelOverride.effectiveModel,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { IssueDetailPanelProps } from '../types';
|
||||
import { isValidationStale } from '../utils';
|
||||
import { ModelOverrideTrigger } from '@/components/shared';
|
||||
|
||||
export function IssueDetailPanel({
|
||||
issue,
|
||||
@@ -27,6 +28,7 @@ export function IssueDetailPanel({
|
||||
onClose,
|
||||
onShowRevalidateConfirm,
|
||||
formatDate,
|
||||
modelOverride,
|
||||
}: IssueDetailPanelProps) {
|
||||
const isValidating = validatingIssues.has(issue.number);
|
||||
const cached = cachedValidations.get(issue.number);
|
||||
@@ -83,10 +85,23 @@ export function IssueDetailPanel({
|
||||
<Clock className="h-4 w-4 mr-1 text-yellow-500" />
|
||||
View (stale)
|
||||
</Button>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={modelOverride.effectiveModel}
|
||||
onModelChange={modelOverride.setOverride}
|
||||
phase="validationModel"
|
||||
isOverridden={modelOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, { forceRevalidate: true })}
|
||||
onClick={() =>
|
||||
onValidateIssue(issue, {
|
||||
forceRevalidate: true,
|
||||
model: modelOverride.effectiveModel,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Re-validate
|
||||
@@ -96,10 +111,24 @@ export function IssueDetailPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="default" size="sm" onClick={() => onValidateIssue(issue)}>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Validate with AI
|
||||
</Button>
|
||||
<>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={modelOverride.effectiveModel}
|
||||
onModelChange={modelOverride.setOverride}
|
||||
phase="validationModel"
|
||||
isOverridden={modelOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, { model: modelOverride.effectiveModel })}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Validate with AI
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenInGitHub(issue.url)}>
|
||||
|
||||
@@ -205,8 +205,8 @@ export function useIssueValidation({
|
||||
}, []);
|
||||
|
||||
const handleValidateIssue = useCallback(
|
||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean } = {}) => {
|
||||
const { forceRevalidate = false } = options;
|
||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean; model?: string } = {}) => {
|
||||
const { forceRevalidate = false, model } = options;
|
||||
|
||||
if (!currentProject?.path) {
|
||||
toast.error('No project selected');
|
||||
@@ -233,6 +233,9 @@ export function useIssueValidation({
|
||||
description: 'You will be notified when the analysis is complete',
|
||||
});
|
||||
|
||||
// Use provided model override or fall back to global validationModel
|
||||
const modelToUse = model || validationModel;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api.github?.validateIssue) {
|
||||
@@ -244,7 +247,7 @@ export function useIssueValidation({
|
||||
issueBody: issue.body || '',
|
||||
issueLabels: issue.labels.map((l) => l.name),
|
||||
},
|
||||
validationModel
|
||||
modelToUse
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitHubIssue, StoredValidation } from '@/lib/electron';
|
||||
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
||||
|
||||
export interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
@@ -18,11 +19,21 @@ export interface IssueDetailPanelProps {
|
||||
cachedValidations: Map<number, StoredValidation>;
|
||||
onValidateIssue: (
|
||||
issue: GitHubIssue,
|
||||
options?: { showDialog?: boolean; forceRevalidate?: boolean }
|
||||
options?: {
|
||||
showDialog?: boolean;
|
||||
forceRevalidate?: boolean;
|
||||
model?: ModelAlias | CursorModelId;
|
||||
}
|
||||
) => Promise<void>;
|
||||
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
|
||||
onOpenInGitHub: (url: string) => void;
|
||||
onClose: () => void;
|
||||
onShowRevalidateConfirm: () => void;
|
||||
formatDate: (date: string) => string;
|
||||
/** Model override state */
|
||||
modelOverride: {
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
isOverridden: boolean;
|
||||
setOverride: (model: ModelAlias | CursorModelId | null) => void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Brain, Bot, Terminal } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
AIProfile,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
CursorModelId,
|
||||
@@ -42,7 +42,7 @@ export function ProfileForm({
|
||||
description: profile.description || '',
|
||||
provider: (profile.provider || 'claude') as ModelProvider,
|
||||
// Claude-specific
|
||||
model: profile.model || ('sonnet' as AgentModel),
|
||||
model: profile.model || ('sonnet' as ModelAlias),
|
||||
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
|
||||
// Cursor-specific
|
||||
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
|
||||
@@ -62,7 +62,7 @@ export function ProfileForm({
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelChange = (model: AgentModel) => {
|
||||
const handleModelChange = (model: ModelAlias) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
model,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
|
||||
// Icon mapping for profiles
|
||||
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
@@ -22,7 +22,7 @@ export const ICON_OPTIONS = [
|
||||
];
|
||||
|
||||
// Model options for the form
|
||||
export const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
|
||||
export const CLAUDE_MODELS: { id: ModelAlias; label: string }[] = [
|
||||
{ id: 'haiku', label: 'Claude Haiku' },
|
||||
{ id: 'sonnet', label: 'Claude Sonnet' },
|
||||
{ id: 'opus', label: 'Claude Opus' },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentModel, ModelProvider, AIProfile } from '@automaker/types';
|
||||
import type { ModelAlias, ModelProvider, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
// Helper to determine provider from model (legacy, always returns 'claude')
|
||||
export function getProviderFromModel(model: AgentModel): ModelProvider {
|
||||
export function getProviderFromModel(model: ModelAlias): ModelProvider {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { AIProfile } from '@/store/app-store';
|
||||
import type { AgentModel } from '@automaker/types';
|
||||
import type { ModelAlias } from '@automaker/types';
|
||||
|
||||
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
|
||||
@@ -36,7 +36,7 @@ interface FeatureDefaultsSectionProps {
|
||||
defaultRequirePlanApproval: boolean;
|
||||
defaultAIProfileId: string | null;
|
||||
aiProfiles: AIProfile[];
|
||||
validationModel: AgentModel;
|
||||
validationModel: ModelAlias;
|
||||
onShowProfilesOnlyChange: (value: boolean) => void;
|
||||
onDefaultSkipTestsChange: (value: boolean) => void;
|
||||
onEnableDependencyBlockingChange: (value: boolean) => void;
|
||||
@@ -44,7 +44,7 @@ interface FeatureDefaultsSectionProps {
|
||||
onDefaultPlanningModeChange: (value: PlanningMode) => void;
|
||||
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
|
||||
onDefaultAIProfileIdChange: (value: string | null) => void;
|
||||
onValidationModelChange: (value: AgentModel) => void;
|
||||
onValidationModelChange: (value: ModelAlias) => void;
|
||||
}
|
||||
|
||||
export function FeatureDefaultsSection({
|
||||
@@ -243,7 +243,7 @@ export function FeatureDefaultsSection({
|
||||
<Label className="text-foreground font-medium">Issue Validation Model</Label>
|
||||
<Select
|
||||
value={validationModel}
|
||||
onValueChange={(v: string) => onValidationModelChange(v as AgentModel)}
|
||||
onValueChange={(v: string) => onValidationModelChange(v as ModelAlias)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-8" data-testid="validation-model-select">
|
||||
<SelectValue />
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { AgentModel, CursorModelId } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import { stripProviderPrefix } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
|
||||
interface PhaseModelSelectorProps {
|
||||
label: string;
|
||||
description: string;
|
||||
value: AgentModel | CursorModelId;
|
||||
onChange: (model: AgentModel | CursorModelId) => void;
|
||||
value: ModelAlias | CursorModelId;
|
||||
onChange: (model: ModelAlias | CursorModelId) => void;
|
||||
}
|
||||
|
||||
export function PhaseModelSelector({
|
||||
@@ -20,13 +21,10 @@ export function PhaseModelSelector({
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Check if current value is a Claude model or Cursor model
|
||||
const isClaudeModel = (v: string) => ['haiku', 'sonnet', 'opus'].includes(v);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -50,7 +48,7 @@ export function PhaseModelSelector({
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => onChange(model.id as AgentModel)}
|
||||
onClick={() => onChange(model.id as ModelAlias)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
||||
'transition-all duration-150',
|
||||
@@ -75,7 +73,7 @@ export function PhaseModelSelector({
|
||||
|
||||
{/* Cursor Models */}
|
||||
{availableCursorModels.map((model) => {
|
||||
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
const isActive = value === cursorId;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Terminal, Info } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CursorModelId, CursorModelConfig } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
@@ -33,6 +34,7 @@ export function CursorSettingsTab() {
|
||||
// Global settings from store
|
||||
const { enabledCursorModels, cursorDefaultModel, setCursorDefaultModel, toggleCursorModel } =
|
||||
useAppStore();
|
||||
const { setCursorCliStatus } = useSetupStore();
|
||||
|
||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -48,11 +50,24 @@ export function CursorSettingsTab() {
|
||||
const statusResult = await api.setup.getCursorStatus();
|
||||
|
||||
if (statusResult.success) {
|
||||
setStatus({
|
||||
const newStatus = {
|
||||
installed: statusResult.installed ?? false,
|
||||
version: statusResult.version ?? undefined,
|
||||
authenticated: statusResult.auth?.authenticated ?? false,
|
||||
method: statusResult.auth?.method,
|
||||
};
|
||||
setStatus(newStatus);
|
||||
|
||||
// Also update the global setup store so other components can access the status
|
||||
setCursorCliStatus({
|
||||
installed: newStatus.installed,
|
||||
version: newStatus.version,
|
||||
auth: newStatus.authenticated
|
||||
? {
|
||||
authenticated: true,
|
||||
method: newStatus.method || 'unknown',
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -61,7 +76,7 @@ export function CursorSettingsTab() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [setCursorCliStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
46
apps/ui/src/hooks/use-cursor-status-init.ts
Normal file
46
apps/ui/src/hooks/use-cursor-status-init.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
|
||||
/**
|
||||
* Hook to initialize Cursor CLI status on app startup.
|
||||
* This ensures the cursorCliStatus is available in the setup store
|
||||
* before the user opens feature dialogs.
|
||||
*/
|
||||
export function useCursorStatusInit() {
|
||||
const { setCursorCliStatus, cursorCliStatus } = useSetupStore();
|
||||
const initialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only initialize once per session
|
||||
if (initialized.current || cursorCliStatus !== null) {
|
||||
return;
|
||||
}
|
||||
initialized.current = true;
|
||||
|
||||
const initCursorStatus = async () => {
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const statusResult = await api.setup.getCursorStatus();
|
||||
|
||||
if (statusResult.success) {
|
||||
setCursorCliStatus({
|
||||
installed: statusResult.installed ?? false,
|
||||
version: statusResult.version ?? undefined,
|
||||
auth: statusResult.auth?.authenticated
|
||||
? {
|
||||
authenticated: true,
|
||||
method: statusResult.auth.method || 'unknown',
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - cursor is optional
|
||||
console.debug('[CursorStatusInit] Failed to check cursor status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initCursorStatus();
|
||||
}, [setCursorCliStatus, cursorCliStatus]);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
IssueValidationResponse,
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
} from '@automaker/types';
|
||||
import { getJSON, setJSON, removeItem } from './storage';
|
||||
|
||||
@@ -198,7 +198,7 @@ export interface GitHubAPI {
|
||||
validateIssue: (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput,
|
||||
model?: AgentModel
|
||||
model?: ModelAlias
|
||||
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
|
||||
/** Check validation status for an issue or all issues */
|
||||
getValidationStatus: (
|
||||
@@ -2734,7 +2734,7 @@ function createMockGitHubAPI(): GitHubAPI {
|
||||
mergedPRs: [],
|
||||
};
|
||||
},
|
||||
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: AgentModel) => {
|
||||
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: ModelAlias) => {
|
||||
console.log('[Mock] Starting async validation:', { projectPath, issue, model });
|
||||
|
||||
// Simulate async validation in background
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { AgentModel } from '@/store/app-store';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -9,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
|
||||
/**
|
||||
* Determine if the current model supports extended thinking controls
|
||||
*/
|
||||
export function modelSupportsThinking(_model?: AgentModel | string): boolean {
|
||||
export function modelSupportsThinking(_model?: ModelAlias | string): boolean {
|
||||
// All Claude models support thinking
|
||||
return true;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function modelSupportsThinking(_model?: AgentModel | string): boolean {
|
||||
/**
|
||||
* Get display name for a model
|
||||
*/
|
||||
export function getModelDisplayName(model: AgentModel | string): string {
|
||||
export function getModelDisplayName(model: ModelAlias | string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
haiku: 'Claude Haiku',
|
||||
sonnet: 'Claude Sonnet',
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Project, TrashedProject } from '@/lib/electron';
|
||||
import type {
|
||||
Feature as BaseFeature,
|
||||
FeatureImagePath,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
PlanningMode,
|
||||
AIProfile,
|
||||
CursorModelId,
|
||||
@@ -13,8 +13,8 @@ import type {
|
||||
} from '@automaker/types';
|
||||
import { getAllCursorModelIds, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||
|
||||
// Re-export ThemeMode for convenience
|
||||
export type { ThemeMode };
|
||||
// Re-export types for convenience
|
||||
export type { ThemeMode, ModelAlias };
|
||||
|
||||
export type ViewMode =
|
||||
| 'welcome'
|
||||
@@ -477,10 +477,10 @@ export interface AppState {
|
||||
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
||||
|
||||
// Enhancement Model Settings
|
||||
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
|
||||
enhancementModel: ModelAlias; // Model used for feature enhancement (default: sonnet)
|
||||
|
||||
// Validation Model Settings
|
||||
validationModel: AgentModel; // Model used for GitHub issue validation (default: opus)
|
||||
validationModel: ModelAlias; // Model used for GitHub issue validation (default: opus)
|
||||
|
||||
// Phase Model Settings - per-phase AI model configuration
|
||||
phaseModels: PhaseModelConfig;
|
||||
@@ -760,13 +760,13 @@ export interface AppActions {
|
||||
setMuteDoneSound: (muted: boolean) => void;
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model: AgentModel) => void;
|
||||
setEnhancementModel: (model: ModelAlias) => void;
|
||||
|
||||
// Validation Model actions
|
||||
setValidationModel: (model: AgentModel) => void;
|
||||
setValidationModel: (model: ModelAlias) => void;
|
||||
|
||||
// Phase Model actions
|
||||
setPhaseModel: (phase: PhaseModelKey, model: AgentModel | CursorModelId) => void;
|
||||
setPhaseModel: (phase: PhaseModelKey, model: ModelAlias | CursorModelId) => void;
|
||||
setPhaseModels: (models: Partial<PhaseModelConfig>) => void;
|
||||
resetPhaseModels: () => void;
|
||||
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
* - Handles multiple model sources with priority
|
||||
*/
|
||||
|
||||
import { CLAUDE_MODEL_MAP, CURSOR_MODEL_MAP, DEFAULT_MODELS } from '@automaker/types';
|
||||
import {
|
||||
CLAUDE_MODEL_MAP,
|
||||
CURSOR_MODEL_MAP,
|
||||
DEFAULT_MODELS,
|
||||
PROVIDER_PREFIXES,
|
||||
isCursorModel,
|
||||
stripProviderPrefix,
|
||||
} from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Resolve a model key/alias to a full model string
|
||||
@@ -33,8 +40,8 @@ export function resolveModelString(
|
||||
|
||||
// Cursor model with explicit prefix (e.g., "cursor-composer-1") - pass through unchanged
|
||||
// CursorProvider will strip the prefix when calling the CLI
|
||||
if (modelKey.startsWith('cursor-')) {
|
||||
const cursorModelId = modelKey.replace('cursor-', '');
|
||||
if (modelKey.startsWith(PROVIDER_PREFIXES.cursor)) {
|
||||
const cursorModelId = stripProviderPrefix(modelKey);
|
||||
// Verify it's a valid Cursor model
|
||||
if (cursorModelId in CURSOR_MODEL_MAP) {
|
||||
console.log(
|
||||
@@ -50,7 +57,7 @@ export function resolveModelString(
|
||||
// Check if it's a bare Cursor model ID (e.g., "composer-1", "auto", "gpt-4o")
|
||||
if (modelKey in CURSOR_MODEL_MAP) {
|
||||
// Return with cursor- prefix so provider routing works correctly
|
||||
const prefixedModel = `cursor-${modelKey}`;
|
||||
const prefixedModel = `${PROVIDER_PREFIXES.cursor}${modelKey}`;
|
||||
console.log(
|
||||
`[ModelResolver] Detected bare Cursor model ID: "${modelKey}" -> "${prefixedModel}"`
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ export type { ErrorType, ErrorInfo } from './error.js';
|
||||
export type { ImageData, ImageContentBlock } from './image.js';
|
||||
|
||||
// Model types and constants
|
||||
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, type ModelAlias, type AgentModel } from './model.js';
|
||||
export { CLAUDE_MODEL_MAP, DEFAULT_MODELS, type ModelAlias } from './model.js';
|
||||
|
||||
// Event types
|
||||
export type { EventType, EventCallback } from './event.js';
|
||||
@@ -114,3 +114,15 @@ export type {
|
||||
// Cursor types
|
||||
export * from './cursor-models.js';
|
||||
export * from './cursor-cli.js';
|
||||
|
||||
// Provider utilities
|
||||
export {
|
||||
PROVIDER_PREFIXES,
|
||||
isCursorModel,
|
||||
isClaudeModel,
|
||||
getModelProvider,
|
||||
stripProviderPrefix,
|
||||
addProviderPrefix,
|
||||
getBareModelId,
|
||||
normalizeModelString,
|
||||
} from './provider-utils.js';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Types for validating GitHub issues against the codebase using Claude SDK.
|
||||
*/
|
||||
|
||||
import type { AgentModel } from './model.js';
|
||||
import type { ModelAlias } from './model.js';
|
||||
|
||||
/**
|
||||
* Verdict from issue validation
|
||||
@@ -102,7 +102,7 @@ export type IssueValidationEvent =
|
||||
result: IssueValidationResult;
|
||||
projectPath: string;
|
||||
/** Model used for validation (opus, sonnet, haiku) */
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
}
|
||||
| {
|
||||
type: 'issue_validation_error';
|
||||
@@ -127,7 +127,7 @@ export interface StoredValidation {
|
||||
/** ISO timestamp when validation was performed */
|
||||
validatedAt: string;
|
||||
/** Model used for validation (opus, sonnet, haiku) */
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
/** The validation result */
|
||||
result: IssueValidationResult;
|
||||
/** ISO timestamp when user viewed this validation (undefined = not yet viewed) */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* and thinking levels used throughout the application UI.
|
||||
*/
|
||||
|
||||
import type { AgentModel, ThinkingLevel, ModelProvider } from './settings.js';
|
||||
import type { ModelAlias, ThinkingLevel, ModelProvider } from './settings.js';
|
||||
import type { CursorModelId } from './cursor-models.js';
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ import type { CursorModelId } from './cursor-models.js';
|
||||
*/
|
||||
export interface ModelOption {
|
||||
/** Model identifier (supports both Claude and Cursor models) */
|
||||
id: AgentModel | CursorModelId;
|
||||
id: ModelAlias | CursorModelId;
|
||||
/** Display name shown to user */
|
||||
label: string;
|
||||
/** Descriptive text explaining model capabilities */
|
||||
@@ -102,7 +102,7 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
|
||||
* getModelDisplayName("claude-opus-4-20250514"); // "claude-opus-4-20250514"
|
||||
* ```
|
||||
*/
|
||||
export function getModelDisplayName(model: AgentModel | string): string {
|
||||
export function getModelDisplayName(model: ModelAlias | string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
haiku: 'Claude Haiku',
|
||||
sonnet: 'Claude Sonnet',
|
||||
|
||||
@@ -16,9 +16,3 @@ export const DEFAULT_MODELS = {
|
||||
} as const;
|
||||
|
||||
export type ModelAlias = keyof typeof CLAUDE_MODEL_MAP;
|
||||
|
||||
/**
|
||||
* AgentModel - Alias for ModelAlias for backward compatibility
|
||||
* Represents available Claude models: "opus" | "sonnet" | "haiku"
|
||||
*/
|
||||
export type AgentModel = ModelAlias;
|
||||
|
||||
140
libs/types/src/provider-utils.ts
Normal file
140
libs/types/src/provider-utils.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Provider utility functions
|
||||
*
|
||||
* Centralized utilities for determining model providers.
|
||||
* When adding new providers, update these functions instead of
|
||||
* scattering .startsWith() checks throughout the codebase.
|
||||
*/
|
||||
|
||||
import type { ModelProvider } from './settings.js';
|
||||
import { CURSOR_MODEL_MAP, type CursorModelId } from './cursor-models.js';
|
||||
import { CLAUDE_MODEL_MAP } from './model.js';
|
||||
|
||||
/** Provider prefix constants */
|
||||
export const PROVIDER_PREFIXES = {
|
||||
cursor: 'cursor-',
|
||||
// Add new provider prefixes here
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Check if a model string represents a Cursor model
|
||||
*
|
||||
* @param model - Model string to check (e.g., "cursor-composer-1" or "composer-1")
|
||||
* @returns true if the model is a Cursor model
|
||||
*/
|
||||
export function isCursorModel(model: string | undefined | null): boolean {
|
||||
if (!model || typeof model !== 'string') return false;
|
||||
|
||||
// Check for explicit cursor- prefix
|
||||
if (model.startsWith(PROVIDER_PREFIXES.cursor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it's a bare Cursor model ID
|
||||
return model in CURSOR_MODEL_MAP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model string represents a Claude model
|
||||
*
|
||||
* @param model - Model string to check (e.g., "sonnet", "opus", "claude-sonnet-4-20250514")
|
||||
* @returns true if the model is a Claude model
|
||||
*/
|
||||
export function isClaudeModel(model: string | undefined | null): boolean {
|
||||
if (!model || typeof model !== 'string') return false;
|
||||
|
||||
// Check if it's a Claude model alias (haiku, sonnet, opus)
|
||||
if (model in CLAUDE_MODEL_MAP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if it contains 'claude-' in the string (full model ID)
|
||||
return model.includes('claude-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider for a model string
|
||||
*
|
||||
* @param model - Model string to check
|
||||
* @returns The provider type, defaults to 'claude' for unknown models
|
||||
*/
|
||||
export function getModelProvider(model: string | undefined | null): ModelProvider {
|
||||
if (isCursorModel(model)) {
|
||||
return 'cursor';
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the provider prefix from a model string
|
||||
*
|
||||
* @param model - Model string that may have a provider prefix
|
||||
* @returns Model string without provider prefix
|
||||
*
|
||||
* @example
|
||||
* stripProviderPrefix('cursor-composer-1') // 'composer-1'
|
||||
* stripProviderPrefix('sonnet') // 'sonnet'
|
||||
*/
|
||||
export function stripProviderPrefix(model: string): string {
|
||||
if (!model || typeof model !== 'string') return model;
|
||||
|
||||
for (const prefix of Object.values(PROVIDER_PREFIXES)) {
|
||||
if (model.startsWith(prefix)) {
|
||||
return model.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the provider prefix to a model string if not already present
|
||||
*
|
||||
* @param model - Bare model ID
|
||||
* @param provider - Provider to add prefix for
|
||||
* @returns Model string with provider prefix
|
||||
*
|
||||
* @example
|
||||
* addProviderPrefix('composer-1', 'cursor') // 'cursor-composer-1'
|
||||
* addProviderPrefix('cursor-composer-1', 'cursor') // 'cursor-composer-1' (no change)
|
||||
* addProviderPrefix('sonnet', 'claude') // 'sonnet' (Claude doesn't use prefix)
|
||||
*/
|
||||
export function addProviderPrefix(model: string, provider: ModelProvider): string {
|
||||
if (!model || typeof model !== 'string') return model;
|
||||
|
||||
if (provider === 'cursor') {
|
||||
if (!model.startsWith(PROVIDER_PREFIXES.cursor)) {
|
||||
return `${PROVIDER_PREFIXES.cursor}${model}`;
|
||||
}
|
||||
}
|
||||
// Claude models don't use prefixes
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bare model ID from a model string (without provider prefix)
|
||||
*
|
||||
* @param model - Model string that may have a provider prefix
|
||||
* @returns The bare model ID
|
||||
*/
|
||||
export function getBareModelId(model: string): string {
|
||||
return stripProviderPrefix(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a model string to its canonical form
|
||||
* - For Cursor: adds cursor- prefix if missing
|
||||
* - For Claude: returns as-is
|
||||
*
|
||||
* @param model - Model string to normalize
|
||||
* @returns Normalized model string
|
||||
*/
|
||||
export function normalizeModelString(model: string | undefined | null): string {
|
||||
if (!model || typeof model !== 'string') return 'sonnet'; // Default
|
||||
|
||||
// If it's a Cursor model without prefix, add the prefix
|
||||
if (model in CURSOR_MODEL_MAP && !model.startsWith(PROVIDER_PREFIXES.cursor)) {
|
||||
return `${PROVIDER_PREFIXES.cursor}${model}`;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
@@ -6,12 +6,12 @@
|
||||
* (for file I/O via SettingsService) and the UI (for state management and sync).
|
||||
*/
|
||||
|
||||
import type { AgentModel } from './model.js';
|
||||
import type { ModelAlias } from './model.js';
|
||||
import type { CursorModelId } from './cursor-models.js';
|
||||
import { CURSOR_MODEL_MAP, getAllCursorModelIds } from './cursor-models.js';
|
||||
|
||||
// Re-export AgentModel for convenience
|
||||
export type { AgentModel };
|
||||
// Re-export ModelAlias for convenience
|
||||
export type { ModelAlias };
|
||||
|
||||
/**
|
||||
* ThemeMode - Available color themes for the UI
|
||||
@@ -82,25 +82,25 @@ export type ModelProvider = 'claude' | 'cursor';
|
||||
export interface PhaseModelConfig {
|
||||
// Quick tasks - recommend fast/cheap models (Haiku, Cursor auto)
|
||||
/** Model for enhancing feature names and descriptions */
|
||||
enhancementModel: AgentModel | CursorModelId;
|
||||
enhancementModel: ModelAlias | CursorModelId;
|
||||
/** Model for generating file context descriptions */
|
||||
fileDescriptionModel: AgentModel | CursorModelId;
|
||||
fileDescriptionModel: ModelAlias | CursorModelId;
|
||||
/** Model for analyzing and describing context images */
|
||||
imageDescriptionModel: AgentModel | CursorModelId;
|
||||
imageDescriptionModel: ModelAlias | CursorModelId;
|
||||
|
||||
// Validation tasks - recommend smart models (Sonnet, Opus)
|
||||
/** Model for validating and improving GitHub issues */
|
||||
validationModel: AgentModel | CursorModelId;
|
||||
validationModel: ModelAlias | CursorModelId;
|
||||
|
||||
// Generation tasks - recommend powerful models (Opus, Sonnet)
|
||||
/** Model for generating full application specifications */
|
||||
specGenerationModel: AgentModel | CursorModelId;
|
||||
specGenerationModel: ModelAlias | CursorModelId;
|
||||
/** Model for creating features from specifications */
|
||||
featureGenerationModel: AgentModel | CursorModelId;
|
||||
featureGenerationModel: ModelAlias | CursorModelId;
|
||||
/** Model for reorganizing and prioritizing backlog */
|
||||
backlogPlanningModel: AgentModel | CursorModelId;
|
||||
backlogPlanningModel: ModelAlias | CursorModelId;
|
||||
/** Model for analyzing project structure */
|
||||
projectAnalysisModel: AgentModel | CursorModelId;
|
||||
projectAnalysisModel: ModelAlias | CursorModelId;
|
||||
}
|
||||
|
||||
/** Keys of PhaseModelConfig for type-safe access */
|
||||
@@ -196,7 +196,7 @@ export interface AIProfile {
|
||||
|
||||
// Claude-specific settings
|
||||
/** Which Claude model to use (opus, sonnet, haiku) - only for Claude provider */
|
||||
model?: AgentModel;
|
||||
model?: ModelAlias;
|
||||
/** Extended thinking level for reasoning-based tasks - only for Claude provider */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
|
||||
@@ -338,9 +338,9 @@ export interface GlobalSettings {
|
||||
|
||||
// Legacy AI Model Selection (deprecated - use phaseModels instead)
|
||||
/** @deprecated Use phaseModels.enhancementModel instead */
|
||||
enhancementModel: AgentModel;
|
||||
enhancementModel: ModelAlias;
|
||||
/** @deprecated Use phaseModels.validationModel instead */
|
||||
validationModel: AgentModel;
|
||||
validationModel: ModelAlias;
|
||||
|
||||
// Cursor CLI Settings (global)
|
||||
/** Which Cursor models are available in feature modal (empty = all) */
|
||||
|
||||
@@ -17,8 +17,7 @@ Allow users to configure which AI provider/model to use for each distinct phase
|
||||
These fields already exist in `GlobalSettings` but are not wired up:
|
||||
|
||||
```typescript
|
||||
// libs/types/src/settings.ts - Line ~50
|
||||
enhancementModel: AgentModel; // Currently ignored, hardcoded to 'sonnet'
|
||||
// libs/types/src/settenhancementModel: AgentModel; // Currently ignored, hardcoded to 'sonnet'
|
||||
validationModel: AgentModel; // Currently ignored, hardcoded to 'opus'
|
||||
```
|
||||
|
||||
@@ -27,8 +26,8 @@ validationModel: AgentModel; // Currently ignored, hardcoded to 'opus'
|
||||
| Phase | Location | Current Model | Priority |
|
||||
| ------------------- | -------------------------------- | ------------------ | -------- |
|
||||
| Feature Execution | `auto-mode-service.ts` | Per-feature | ✅ Done |
|
||||
| Enhancement | `enhance.ts` | Hardcoded `sonnet` | P1 |
|
||||
| GitHub Validation | `validate-issue.ts` | Hardcoded `opus` | P1 |
|
||||
| Enhancement | `enhance.ts` | Hardcoded `sonnet` | ✅ Done |
|
||||
| GitHub Validation | `validate-issue.ts` | Hardcoded `opus` | ✅ Done |
|
||||
| File Description | `describe-file.ts` | Hardcoded `haiku` | P2 |
|
||||
| Image Description | `describe-image.ts` | Hardcoded `haiku` | P2 |
|
||||
| App Spec Generation | `generate-spec.ts` | SDK default | P2 |
|
||||
@@ -372,8 +371,8 @@ Phase 8: Quick Model Override Component <- Right after UI for easier testing
|
||||
Phase 9: Integration Points for Override <- Wire override to each feature
|
||||
|
||||
Then wire routes (testing both global + override together):
|
||||
Phase 3: Enhancement Route
|
||||
Phase 4: Validation Route
|
||||
Phase 3: Enhancement Route - Done
|
||||
Phase 4: Validation Route - Broken validation parsing ! need urgent fix to properly validate cursor cli output
|
||||
Phase 5: Context Routes (file/image description)
|
||||
Phase 6: Generation Routes (spec, features)
|
||||
Phase 7: Remaining Routes (backlog, analysis)
|
||||
@@ -676,6 +675,34 @@ Add override trigger in header:
|
||||
|
||||
Add override for validation model:
|
||||
|
||||
for this feature
|
||||
|
||||
- Clear override returns to profile default
|
||||
|
||||
### 9.2 Kanban Card
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`
|
||||
|
||||
Add small gear icon next to "Implement" button:
|
||||
|
||||
- Quick model change before running
|
||||
- Doesn't persist to feature (one-time override)
|
||||
|
||||
### 9.3 Enhancement Dialog
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/enhance-dialog.tsx`
|
||||
|
||||
Add override trigger in header:
|
||||
|
||||
- Default from global settings
|
||||
- Override for this enhancement only
|
||||
|
||||
### 9.4 GitHub Import
|
||||
|
||||
**File**: `apps/ui/src/components/views/github-view/`
|
||||
|
||||
Add override for validation model:
|
||||
|
||||
- Default from global settings
|
||||
- Override for this import session
|
||||
|
||||
|
||||
Reference in New Issue
Block a user