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