mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat(ui): Enhance AI model handling with Cursor support
- Refactor model handling to support both Claude and Cursor models across various components. - Introduce `stripProviderPrefix` utility for consistent model ID processing. - Update `CursorProvider` to utilize `isCursorModel` for model validation. - Implement model override functionality in GitHub issue validation and enhancement routes. - Add `useCursorStatusInit` hook to initialize Cursor CLI status on app startup. - Update UI components to reflect changes in model selection and validation processes. This update improves the flexibility of AI model usage and enhances user experience by allowing quick model overrides.
This commit is contained in:
@@ -28,6 +28,7 @@ import type {
|
||||
ModelDefinition,
|
||||
ContentBlock,
|
||||
} from './types.js';
|
||||
import { stripProviderPrefix } from '@automaker/types';
|
||||
import {
|
||||
type CursorStreamEvent,
|
||||
type CursorSystemEvent,
|
||||
@@ -115,10 +116,7 @@ export class CursorProvider extends CliProvider {
|
||||
|
||||
buildCliArgs(options: ExecuteOptions): string[] {
|
||||
// Extract model (strip 'cursor-' prefix if present)
|
||||
let model = options.model || 'auto';
|
||||
if (model.startsWith('cursor-')) {
|
||||
model = model.substring(7);
|
||||
}
|
||||
const model = stripProviderPrefix(options.model || 'auto');
|
||||
|
||||
// Build prompt content
|
||||
let promptText: string;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import type { InstallationStatus, ModelDefinition } from './types.js';
|
||||
import { CURSOR_MODEL_MAP, type ModelProvider } from '@automaker/types';
|
||||
import { isCursorModel, type ModelProvider } from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Provider registration entry
|
||||
@@ -181,14 +181,6 @@ registerProvider('claude', {
|
||||
// Register Cursor provider
|
||||
registerProvider('cursor', {
|
||||
factory: () => new CursorProvider(),
|
||||
canHandleModel: (model: string) => {
|
||||
// Check for explicit cursor prefix
|
||||
if (model.startsWith('cursor-')) {
|
||||
return true;
|
||||
}
|
||||
// Check if it's a known Cursor model ID
|
||||
const modelId = model.replace('cursor-', '');
|
||||
return modelId in CURSOR_MODEL_MAP;
|
||||
},
|
||||
canHandleModel: (model: string) => isCursorModel(model),
|
||||
priority: 10, // Higher priority - check Cursor models first
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* POST /enhance-prompt endpoint - Enhance user input text
|
||||
*
|
||||
* Uses Claude AI to enhance text based on the specified enhancement mode.
|
||||
* Uses Claude AI or Cursor to enhance text based on the specified enhancement mode.
|
||||
* Supports modes: improve, technical, simplify, acceptance
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,8 @@ import type { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
import { resolveModelString } from '@automaker/model-resolver';
|
||||
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||
import { CLAUDE_MODEL_MAP, isCursorModel } from '@automaker/types';
|
||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||
import {
|
||||
getSystemPrompt,
|
||||
buildUserPrompt,
|
||||
@@ -80,6 +81,40 @@ async function extractTextFromStream(
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute enhancement using Cursor provider
|
||||
*
|
||||
* @param prompt - The enhancement prompt
|
||||
* @param model - The Cursor model to use
|
||||
* @returns The enhanced text
|
||||
*/
|
||||
async function executeWithCursor(prompt: string, model: string): Promise<string> {
|
||||
const provider = ProviderFactory.getProviderForModel(model);
|
||||
|
||||
let responseText = '';
|
||||
|
||||
for await (const msg of provider.executeQuery({
|
||||
prompt,
|
||||
model,
|
||||
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
|
||||
})) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
responseText += block.text;
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||
// Use result if it's a final accumulated message
|
||||
if (msg.result.length > responseText.length) {
|
||||
responseText = msg.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the enhance request handler
|
||||
*
|
||||
@@ -140,24 +175,36 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
|
||||
|
||||
logger.debug(`Using model: ${resolvedModel}`);
|
||||
|
||||
// Call Claude SDK with minimal configuration for text transformation
|
||||
// Key: no tools, just text completion
|
||||
const stream = query({
|
||||
prompt: userPrompt,
|
||||
options: {
|
||||
model: resolvedModel,
|
||||
systemPrompt,
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
permissionMode: 'acceptEdits',
|
||||
},
|
||||
});
|
||||
let enhancedText: string;
|
||||
|
||||
// Extract the enhanced text from the response
|
||||
const enhancedText = await extractTextFromStream(stream);
|
||||
// Route to appropriate provider based on model
|
||||
if (isCursorModel(resolvedModel)) {
|
||||
// Use Cursor provider for Cursor models
|
||||
logger.info(`Using Cursor provider for model: ${resolvedModel}`);
|
||||
|
||||
// Cursor doesn't have a separate system prompt concept, so combine them
|
||||
const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
|
||||
enhancedText = await executeWithCursor(combinedPrompt, resolvedModel);
|
||||
} else {
|
||||
// Use Claude SDK for Claude models
|
||||
logger.info(`Using Claude provider for model: ${resolvedModel}`);
|
||||
|
||||
const stream = query({
|
||||
prompt: userPrompt,
|
||||
options: {
|
||||
model: resolvedModel,
|
||||
systemPrompt,
|
||||
maxTurns: 1,
|
||||
allowedTools: [],
|
||||
permissionMode: 'acceptEdits',
|
||||
},
|
||||
});
|
||||
|
||||
enhancedText = await extractTextFromStream(stream);
|
||||
}
|
||||
|
||||
if (!enhancedText || enhancedText.trim().length === 0) {
|
||||
logger.warn('Received empty response from Claude');
|
||||
logger.warn('Received empty response from AI');
|
||||
const response: EnhanceErrorResponse = {
|
||||
success: false,
|
||||
error: 'Failed to generate enhanced text - empty response',
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
/**
|
||||
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK (async)
|
||||
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK or Cursor (async)
|
||||
*
|
||||
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
|
||||
* Runs asynchronously and emits events for progress and completion.
|
||||
* Supports both Claude models and Cursor models.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from 'express';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import type { IssueValidationResult, IssueValidationEvent, AgentModel } from '@automaker/types';
|
||||
import type {
|
||||
IssueValidationResult,
|
||||
IssueValidationEvent,
|
||||
ModelAlias,
|
||||
CursorModelId,
|
||||
} from '@automaker/types';
|
||||
import { isCursorModel } from '@automaker/types';
|
||||
import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
|
||||
import { writeValidation } from '../../../lib/validation-storage.js';
|
||||
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||
import {
|
||||
issueValidationSchema,
|
||||
ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
@@ -26,8 +34,8 @@ import {
|
||||
import type { SettingsService } from '../../../services/settings-service.js';
|
||||
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
|
||||
|
||||
/** Valid model values for validation */
|
||||
const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
/** Valid Claude model values for validation */
|
||||
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
|
||||
|
||||
/**
|
||||
* Request body for issue validation
|
||||
@@ -38,8 +46,8 @@ interface ValidateIssueRequestBody {
|
||||
issueTitle: string;
|
||||
issueBody: string;
|
||||
issueLabels?: string[];
|
||||
/** Model to use for validation (opus, sonnet, haiku) */
|
||||
model?: AgentModel;
|
||||
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
|
||||
model?: ModelAlias | CursorModelId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +55,7 @@ interface ValidateIssueRequestBody {
|
||||
*
|
||||
* Emits events for start, progress, complete, and error.
|
||||
* Stores result on completion.
|
||||
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing).
|
||||
*/
|
||||
async function runValidation(
|
||||
projectPath: string,
|
||||
@@ -54,7 +63,7 @@ async function runValidation(
|
||||
issueTitle: string,
|
||||
issueBody: string,
|
||||
issueLabels: string[] | undefined,
|
||||
model: AgentModel,
|
||||
model: ModelAlias | CursorModelId,
|
||||
events: EventEmitter,
|
||||
abortController: AbortController,
|
||||
settingsService?: SettingsService
|
||||
@@ -79,65 +88,133 @@ async function runValidation(
|
||||
// Build the prompt
|
||||
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels);
|
||||
|
||||
// Load autoLoadClaudeMd setting
|
||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||
projectPath,
|
||||
settingsService,
|
||||
'[ValidateIssue]'
|
||||
);
|
||||
|
||||
// Create SDK options with structured output and abort controller
|
||||
const options = createSuggestionsOptions({
|
||||
cwd: projectPath,
|
||||
model,
|
||||
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
outputFormat: {
|
||||
type: 'json_schema',
|
||||
schema: issueValidationSchema as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute the query
|
||||
const stream = query({ prompt, options });
|
||||
let validationResult: IssueValidationResult | null = null;
|
||||
let responseText = '';
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Collect assistant text for debugging and emit progress
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
// Route to appropriate provider based on model
|
||||
if (isCursorModel(model)) {
|
||||
// Use Cursor provider for Cursor models
|
||||
logger.info(`Using Cursor provider for validation with model: ${model}`);
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
const provider = ProviderFactory.getProviderForModel(model);
|
||||
|
||||
// For Cursor, include the system prompt and schema in the user prompt
|
||||
const cursorPrompt = `${ISSUE_VALIDATION_SYSTEM_PROMPT}
|
||||
|
||||
You MUST respond with a valid JSON object matching this schema:
|
||||
${JSON.stringify(issueValidationSchema, null, 2)}
|
||||
|
||||
${prompt}`;
|
||||
|
||||
for await (const msg of provider.executeQuery({
|
||||
prompt: cursorPrompt,
|
||||
model,
|
||||
cwd: projectPath,
|
||||
})) {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
responseText += block.text;
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||
// Use result if it's a final accumulated message
|
||||
if (msg.result.length > responseText.length) {
|
||||
responseText = msg.result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract structured output on success
|
||||
if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||
if (resultMsg.structured_output) {
|
||||
validationResult = resultMsg.structured_output;
|
||||
logger.debug('Received structured output:', validationResult);
|
||||
// Parse JSON from the response text
|
||||
if (responseText) {
|
||||
try {
|
||||
// Try to extract JSON from response (it might be wrapped in markdown code blocks)
|
||||
let jsonStr = responseText;
|
||||
|
||||
// Remove markdown code blocks if present
|
||||
const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
if (jsonMatch) {
|
||||
jsonStr = jsonMatch[1].trim();
|
||||
}
|
||||
|
||||
validationResult = JSON.parse(jsonStr) as IssueValidationResult;
|
||||
logger.debug('Parsed validation result from Cursor response:', validationResult);
|
||||
} catch (parseError) {
|
||||
logger.error('Failed to parse JSON from Cursor response:', parseError);
|
||||
logger.debug('Raw response:', responseText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use Claude SDK for Claude models
|
||||
logger.info(`Using Claude provider for validation with model: ${model}`);
|
||||
|
||||
// Handle errors
|
||||
if (msg.type === 'result') {
|
||||
const resultMsg = msg as { subtype?: string };
|
||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||
logger.error('Failed to produce valid structured output after retries');
|
||||
throw new Error('Could not produce valid validation output');
|
||||
// Load autoLoadClaudeMd setting
|
||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
||||
projectPath,
|
||||
settingsService,
|
||||
'[ValidateIssue]'
|
||||
);
|
||||
|
||||
// Create SDK options with structured output and abort controller
|
||||
const options = createSuggestionsOptions({
|
||||
cwd: projectPath,
|
||||
model: model as ModelAlias,
|
||||
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
|
||||
abortController,
|
||||
autoLoadClaudeMd,
|
||||
outputFormat: {
|
||||
type: 'json_schema',
|
||||
schema: issueValidationSchema as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
|
||||
// Execute the query
|
||||
const stream = query({ prompt, options });
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Collect assistant text for debugging and emit progress
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text;
|
||||
|
||||
// Emit progress event
|
||||
const progressEvent: IssueValidationEvent = {
|
||||
type: 'issue_validation_progress',
|
||||
issueNumber,
|
||||
content: block.text,
|
||||
projectPath,
|
||||
};
|
||||
events.emit('issue-validation:event', progressEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract structured output on success
|
||||
if (msg.type === 'result' && msg.subtype === 'success') {
|
||||
const resultMsg = msg as { structured_output?: IssueValidationResult };
|
||||
if (resultMsg.structured_output) {
|
||||
validationResult = resultMsg.structured_output;
|
||||
logger.debug('Received structured output:', validationResult);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if (msg.type === 'result') {
|
||||
const resultMsg = msg as { subtype?: string };
|
||||
if (resultMsg.subtype === 'error_max_structured_output_retries') {
|
||||
logger.error('Failed to produce valid structured output after retries');
|
||||
throw new Error('Could not produce valid validation output');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,11 +222,11 @@ async function runValidation(
|
||||
// Clear timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Require structured output
|
||||
// Require validation result
|
||||
if (!validationResult) {
|
||||
logger.error('No structured output received from Claude SDK');
|
||||
logger.error('No validation result received from AI provider');
|
||||
logger.debug('Raw response text:', responseText);
|
||||
throw new Error('Validation failed: no structured output received');
|
||||
throw new Error('Validation failed: no valid result received');
|
||||
}
|
||||
|
||||
logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`);
|
||||
@@ -239,11 +316,14 @@ export function createValidateIssueHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate model parameter at runtime
|
||||
if (!VALID_MODELS.includes(model)) {
|
||||
// Validate model parameter at runtime - accept Claude models or Cursor models
|
||||
const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias);
|
||||
const isValidCursorModel = isCursorModel(model);
|
||||
|
||||
if (!isValidClaudeModel && !isValidCursorModel) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid model. Must be one of: ${VALID_MODELS.join(', ')}`,
|
||||
error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export type {
|
||||
ThemeMode,
|
||||
KanbanCardDetailLevel,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
PlanningMode,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { RouterProvider } from '@tanstack/react-router';
|
||||
import { router } from './utils/router';
|
||||
import { SplashScreen } from './components/splash-screen';
|
||||
import { useSettingsMigration } from './hooks/use-settings-migration';
|
||||
import { useCursorStatusInit } from './hooks/use-cursor-status-init';
|
||||
import './styles/global.css';
|
||||
import './styles/theme-imports';
|
||||
|
||||
@@ -21,6 +22,9 @@ export default function App() {
|
||||
console.log('[App] Settings migrated to file storage');
|
||||
}
|
||||
|
||||
// Initialize Cursor CLI status at startup
|
||||
useCursorStatusInit();
|
||||
|
||||
const handleSplashComplete = useCallback(() => {
|
||||
sessionStorage.setItem('automaker-splash-shown', 'true');
|
||||
setShowSplash(false);
|
||||
|
||||
@@ -4,14 +4,15 @@ import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { AgentModel, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
import { PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
|
||||
export interface ModelOverrideTriggerProps {
|
||||
/** Current effective model (from global settings or explicit override) */
|
||||
currentModel: AgentModel | CursorModelId;
|
||||
currentModel: ModelAlias | CursorModelId;
|
||||
/** Callback when user selects override */
|
||||
onModelChange: (model: AgentModel | CursorModelId | null) => void;
|
||||
onModelChange: (model: ModelAlias | CursorModelId | null) => void;
|
||||
/** Optional: which phase this is for (shows global default) */
|
||||
phase?: PhaseModelKey;
|
||||
/** Size variants for different contexts */
|
||||
@@ -24,13 +25,13 @@ export interface ModelOverrideTriggerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getModelLabel(modelId: AgentModel | CursorModelId): string {
|
||||
function getModelLabel(modelId: ModelAlias | CursorModelId): string {
|
||||
// Check Claude models
|
||||
const claudeModel = CLAUDE_MODELS.find((m) => m.id === modelId);
|
||||
if (claudeModel) return claudeModel.label;
|
||||
|
||||
// Check Cursor models (without cursor- prefix)
|
||||
const cursorModel = CURSOR_MODELS.find((m) => m.id === `cursor-${modelId}`);
|
||||
const cursorModel = CURSOR_MODELS.find((m) => m.id === `${PROVIDER_PREFIXES.cursor}${modelId}`);
|
||||
if (cursorModel) return cursorModel.label;
|
||||
|
||||
// Check Cursor models (with cursor- prefix)
|
||||
@@ -57,11 +58,11 @@ export function ModelOverrideTrigger({
|
||||
|
||||
// Filter Cursor models to only show enabled ones
|
||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
return enabledCursorModels.includes(cursorId);
|
||||
});
|
||||
|
||||
const handleSelect = (model: AgentModel | CursorModelId) => {
|
||||
const handleSelect = (model: ModelAlias | CursorModelId) => {
|
||||
onModelChange(model);
|
||||
setOpen(false);
|
||||
};
|
||||
@@ -162,7 +163,7 @@ export function ModelOverrideTrigger({
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => handleSelect(model.id as AgentModel)}
|
||||
onClick={() => handleSelect(model.id as ModelAlias)}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-xs font-medium text-center',
|
||||
'transition-all duration-150',
|
||||
@@ -190,7 +191,7 @@ export function ModelOverrideTrigger({
|
||||
</h5>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{availableCursorModels.slice(0, 6).map((model) => {
|
||||
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
const isActive = currentModel === cursorId;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { AgentModel, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, PhaseModelKey } from '@automaker/types';
|
||||
|
||||
export interface UseModelOverrideOptions {
|
||||
/** Which phase this override is for */
|
||||
phase: PhaseModelKey;
|
||||
/** Initial override value (optional) */
|
||||
initialOverride?: AgentModel | CursorModelId | null;
|
||||
initialOverride?: ModelAlias | CursorModelId | null;
|
||||
}
|
||||
|
||||
export interface UseModelOverrideResult {
|
||||
/** The effective model (override or global default) */
|
||||
effectiveModel: AgentModel | CursorModelId;
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
/** Whether the model is currently overridden */
|
||||
isOverridden: boolean;
|
||||
/** Set a model override */
|
||||
setOverride: (model: AgentModel | CursorModelId | null) => void;
|
||||
setOverride: (model: ModelAlias | CursorModelId | null) => void;
|
||||
/** Clear the override and use global default */
|
||||
clearOverride: () => void;
|
||||
/** The global default for this phase */
|
||||
globalDefault: AgentModel | CursorModelId;
|
||||
globalDefault: ModelAlias | CursorModelId;
|
||||
/** The current override value (null if not overridden) */
|
||||
override: AgentModel | CursorModelId | null;
|
||||
override: ModelAlias | CursorModelId | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ export function useModelOverride({
|
||||
initialOverride = null,
|
||||
}: UseModelOverrideOptions): UseModelOverrideResult {
|
||||
const { phaseModels } = useAppStore();
|
||||
const [override, setOverrideState] = useState<AgentModel | CursorModelId | null>(initialOverride);
|
||||
const [override, setOverrideState] = useState<ModelAlias | CursorModelId | null>(initialOverride);
|
||||
|
||||
const globalDefault = phaseModels[phase];
|
||||
|
||||
@@ -63,7 +63,7 @@ export function useModelOverride({
|
||||
|
||||
const isOverridden = override !== null;
|
||||
|
||||
const setOverride = useCallback((model: AgentModel | CursorModelId | null) => {
|
||||
const setOverride = useCallback((model: ModelAlias | CursorModelId | null) => {
|
||||
setOverrideState(model);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { useAppStore, type AgentModel } from '@/store/app-store';
|
||||
import { useAppStore, type ModelAlias } from '@/store/app-store';
|
||||
import type { CursorModelId } from '@automaker/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ImageDropZone } from '@/components/ui/image-drop-zone';
|
||||
@@ -63,7 +64,7 @@ export function AgentView() {
|
||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||
const [showSessionManager, setShowSessionManager] = useState(true);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [selectedModel, setSelectedModel] = useState<AgentModel>('sonnet');
|
||||
const [selectedModel, setSelectedModel] = useState<ModelAlias | CursorModelId>('sonnet');
|
||||
|
||||
// Track if initial session has been loaded
|
||||
const initialSessionLoadedRef = useRef(false);
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { useAppStore, Feature } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import type { AutoModeEvent } from '@/types/electron';
|
||||
import type { BacklogPlanResult } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId, BacklogPlanResult } from '@automaker/types';
|
||||
import { pathsEqual } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
|
||||
@@ -25,7 +25,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
useAppStore,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
FeatureImage,
|
||||
AIProfile,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
PlanningModeSelector,
|
||||
AncestorContextSection,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
formatAncestorContextForPrompt,
|
||||
type AncestorContext,
|
||||
} from '@automaker/dependency-resolver';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface AddFeatureDialogProps {
|
||||
open: boolean;
|
||||
@@ -66,7 +68,7 @@ interface AddFeatureDialogProps {
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string; // Can be empty string to use current branch
|
||||
priority: number;
|
||||
@@ -115,7 +117,7 @@ export function AddFeatureDialog({
|
||||
imagePaths: [] as DescriptionImagePath[],
|
||||
textFilePaths: [] as DescriptionTextFilePath[],
|
||||
skipTests: false,
|
||||
model: 'opus' as AgentModel,
|
||||
model: 'opus' as ModelAlias,
|
||||
thinkingLevel: 'none' as ThinkingLevel,
|
||||
branchName: '',
|
||||
priority: 2 as number, // Default to medium priority
|
||||
@@ -136,14 +138,12 @@ export function AddFeatureDialog({
|
||||
const [ancestors, setAncestors] = useState<AncestorContext[]>([]);
|
||||
const [selectedAncestorIds, setSelectedAncestorIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Get enhancement model, planning mode defaults, and worktrees setting from store
|
||||
const {
|
||||
enhancementModel,
|
||||
defaultPlanningMode,
|
||||
defaultRequirePlanApproval,
|
||||
defaultAIProfileId,
|
||||
useWorktrees,
|
||||
} = useAppStore();
|
||||
// Get planning mode defaults and worktrees setting from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, defaultAIProfileId, useWorktrees } =
|
||||
useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
// Sync defaults when dialog opens
|
||||
useEffect(() => {
|
||||
@@ -294,7 +294,7 @@ export function AddFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
newFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -315,11 +315,11 @@ export function AddFeatureDialog({
|
||||
const handleModelSelect = (model: string) => {
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursorModel = model.startsWith('cursor-');
|
||||
const isCursor = isCursorModel(model);
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: model as AgentModel,
|
||||
thinkingLevel: isCursorModel
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? newFeature.thinkingLevel
|
||||
@@ -330,10 +330,10 @@ export function AddFeatureDialog({
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setNewFeature({
|
||||
...newFeature,
|
||||
model: cursorModel as AgentModel,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
@@ -347,8 +347,9 @@ export function AddFeatureDialog({
|
||||
};
|
||||
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCursorModel = newFeature.model.startsWith('cursor-');
|
||||
const newModelAllowsThinking = !isCursorModel && modelSupportsThinking(newFeature.model);
|
||||
const isCurrentModelCursor = isCursorModel(newFeature.model);
|
||||
const newModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(newFeature.model || 'sonnet');
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleDialogClose}>
|
||||
@@ -480,6 +481,15 @@ export function AddFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category (optional)</Label>
|
||||
@@ -540,7 +550,7 @@ export function AddFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={newFeature.model}
|
||||
selectedThinkingLevel={newFeature.thinkingLevel}
|
||||
selectedCursorModel={isCursorModel ? newFeature.model : undefined}
|
||||
selectedCursorModel={isCurrentModelCursor ? newFeature.model : undefined}
|
||||
onSelect={handleProfileSelect}
|
||||
showManageLink
|
||||
onManageLinkClick={() => {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { getElectronAPI } from '@/lib/electron';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import {
|
||||
Feature,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
AIProfile,
|
||||
useAppStore,
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
BranchSelector,
|
||||
PlanningModeSelector,
|
||||
} from '../shared';
|
||||
import { ModelOverrideTrigger, useModelOverride } from '@/components/shared';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
||||
import { isCursorModel, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
|
||||
interface EditFeatureDialogProps {
|
||||
feature: Feature | null;
|
||||
@@ -65,7 +67,7 @@ interface EditFeatureDialogProps {
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
textFilePaths: DescriptionTextFilePath[];
|
||||
@@ -117,8 +119,11 @@ export function EditFeatureDialog({
|
||||
feature?.requirePlanApproval ?? false
|
||||
);
|
||||
|
||||
// Get enhancement model and worktrees setting from store
|
||||
const { enhancementModel, useWorktrees } = useAppStore();
|
||||
// Get worktrees setting from store
|
||||
const { useWorktrees } = useAppStore();
|
||||
|
||||
// Enhancement model override
|
||||
const enhancementOverride = useModelOverride({ phase: 'enhancementModel' });
|
||||
|
||||
useEffect(() => {
|
||||
setEditingFeature(feature);
|
||||
@@ -148,7 +153,7 @@ export function EditFeatureDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as AgentModel;
|
||||
const selectedModel = (editingFeature.model ?? 'opus') as ModelAlias;
|
||||
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
|
||||
? (editingFeature.thinkingLevel ?? 'none')
|
||||
: 'none';
|
||||
@@ -187,22 +192,40 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelect = (model: AgentModel) => {
|
||||
const handleModelSelect = (model: string) => {
|
||||
if (!editingFeature) return;
|
||||
// For Cursor models, thinking is handled by the model itself
|
||||
// For Claude models, check if it supports extended thinking
|
||||
const isCursor = isCursorModel(model);
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel: modelSupportsThinking(model) ? editingFeature.thinkingLevel : 'none',
|
||||
model: model as ModelAlias,
|
||||
thinkingLevel: isCursor
|
||||
? 'none'
|
||||
: modelSupportsThinking(model)
|
||||
? editingFeature.thinkingLevel
|
||||
: 'none',
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSelect = (model: AgentModel, thinkingLevel: ThinkingLevel) => {
|
||||
const handleProfileSelect = (profile: AIProfile) => {
|
||||
if (!editingFeature) return;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model,
|
||||
thinkingLevel,
|
||||
});
|
||||
if (profile.provider === 'cursor') {
|
||||
// Cursor profile - set cursor model
|
||||
const cursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: cursorModel as ModelAlias,
|
||||
thinkingLevel: 'none', // Cursor handles thinking internally
|
||||
});
|
||||
} else {
|
||||
// Claude profile
|
||||
setEditingFeature({
|
||||
...editingFeature,
|
||||
model: profile.model || 'sonnet',
|
||||
thinkingLevel: profile.thinkingLevel || 'none',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnhanceDescription = async () => {
|
||||
@@ -214,7 +237,7 @@ export function EditFeatureDialog({
|
||||
const result = await api.enhancePrompt?.enhance(
|
||||
editingFeature.description,
|
||||
enhancementMode,
|
||||
enhancementModel
|
||||
enhancementOverride.effectiveModel
|
||||
);
|
||||
|
||||
if (result?.success && result.enhancedText) {
|
||||
@@ -232,7 +255,10 @@ export function EditFeatureDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const editModelAllowsThinking = modelSupportsThinking(editingFeature?.model);
|
||||
// Cursor models handle thinking internally, so only show thinking selector for Claude models
|
||||
const isCurrentModelCursor = isCursorModel(editingFeature?.model as string);
|
||||
const editModelAllowsThinking =
|
||||
!isCurrentModelCursor && modelSupportsThinking(editingFeature?.model);
|
||||
|
||||
if (!editingFeature) {
|
||||
return null;
|
||||
@@ -361,6 +387,15 @@ export function EditFeatureDialog({
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Enhance with AI
|
||||
</Button>
|
||||
|
||||
<ModelOverrideTrigger
|
||||
currentModel={enhancementOverride.effectiveModel}
|
||||
onModelChange={enhancementOverride.setOverride}
|
||||
phase="enhancementModel"
|
||||
isOverridden={enhancementOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-category">Category (optional)</Label>
|
||||
@@ -437,6 +472,9 @@ export function EditFeatureDialog({
|
||||
profiles={aiProfiles}
|
||||
selectedModel={editingFeature.model ?? 'opus'}
|
||||
selectedThinkingLevel={editingFeature.thinkingLevel ?? 'none'}
|
||||
selectedCursorModel={
|
||||
isCurrentModelCursor ? (editingFeature.model as string) : undefined
|
||||
}
|
||||
onSelect={handleProfileSelect}
|
||||
testIdPrefix="edit-profile-quick-select"
|
||||
/>
|
||||
@@ -450,7 +488,7 @@ export function EditFeatureDialog({
|
||||
{(!showProfilesOnly || showEditAdvancedOptions) && (
|
||||
<>
|
||||
<ModelSelector
|
||||
selectedModel={(editingFeature.model ?? 'opus') as AgentModel}
|
||||
selectedModel={(editingFeature.model ?? 'opus') as ModelAlias}
|
||||
onModelSelect={handleModelSelect}
|
||||
testIdPrefix="edit-model-select"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
Feature,
|
||||
FeatureImage,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
PlanningMode,
|
||||
useAppStore,
|
||||
@@ -92,7 +92,7 @@ export function useBoardActions({
|
||||
images: FeatureImage[];
|
||||
imagePaths: DescriptionImagePath[];
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
branchName: string;
|
||||
priority: number;
|
||||
@@ -210,7 +210,7 @@ export function useBoardActions({
|
||||
category: string;
|
||||
description: string;
|
||||
skipTests: boolean;
|
||||
model: AgentModel;
|
||||
model: ModelAlias;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
imagePaths: DescriptionImagePath[];
|
||||
branchName: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
|
||||
export type ModelOption = {
|
||||
id: string; // Claude models use AgentModel, Cursor models use "cursor-{id}"
|
||||
id: string; // Claude models use ModelAlias, Cursor models use "cursor-{id}"
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
|
||||
@@ -2,28 +2,19 @@ import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Brain, Bot, Terminal, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel } from '@/store/app-store';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getModelProvider, PROVIDER_PREFIXES, stripProviderPrefix } from '@automaker/types';
|
||||
import type { ModelProvider } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS, ModelOption } from './model-constants';
|
||||
|
||||
interface ModelSelectorProps {
|
||||
selectedModel: string; // Can be AgentModel or "cursor-{id}"
|
||||
selectedModel: string; // Can be ModelAlias or "cursor-{id}"
|
||||
onModelSelect: (model: string) => void;
|
||||
testIdPrefix?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the provider from a model string
|
||||
*/
|
||||
function getProviderFromModelString(model: string): ModelProvider {
|
||||
if (model.startsWith('cursor-')) {
|
||||
return 'cursor';
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
selectedModel,
|
||||
onModelSelect,
|
||||
@@ -32,7 +23,7 @@ export function ModelSelector({
|
||||
const { enabledCursorModels, cursorDefaultModel } = useAppStore();
|
||||
const { cursorCliStatus } = useSetupStore();
|
||||
|
||||
const selectedProvider = getProviderFromModelString(selectedModel);
|
||||
const selectedProvider = getModelProvider(selectedModel);
|
||||
|
||||
// Check if Cursor CLI is available
|
||||
const isCursorAvailable = cursorCliStatus?.installed && cursorCliStatus?.auth?.authenticated;
|
||||
@@ -40,14 +31,14 @@ export function ModelSelector({
|
||||
// Filter Cursor models based on enabled models from global settings
|
||||
const filteredCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
// Extract the cursor model ID from the prefixed ID (e.g., "cursor-auto" -> "auto")
|
||||
const cursorModelId = model.id.replace('cursor-', '');
|
||||
const cursorModelId = stripProviderPrefix(model.id);
|
||||
return enabledCursorModels.includes(cursorModelId as any);
|
||||
});
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
if (provider === 'cursor' && selectedProvider !== 'cursor') {
|
||||
// Switch to Cursor's default model (from global settings)
|
||||
onModelSelect(`cursor-${cursorDefaultModel}`);
|
||||
onModelSelect(`${PROVIDER_PREFIXES.cursor}${cursorDefaultModel}`);
|
||||
} else if (provider === 'claude' && selectedProvider !== 'claude') {
|
||||
// Switch to Claude's default model
|
||||
onModelSelect('sonnet');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Brain, UserCircle, Terminal } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AgentModel, ThinkingLevel, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
|
||||
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
|
||||
import { PROFILE_ICONS } from './model-constants';
|
||||
|
||||
/**
|
||||
@@ -32,7 +32,7 @@ function getProfileThinkingDisplay(profile: AIProfile): string | null {
|
||||
|
||||
interface ProfileQuickSelectProps {
|
||||
profiles: AIProfile[];
|
||||
selectedModel: AgentModel;
|
||||
selectedModel: ModelAlias | CursorModelId;
|
||||
selectedThinkingLevel: ThinkingLevel;
|
||||
selectedCursorModel?: string; // For detecting cursor profile selection
|
||||
onSelect: (profile: AIProfile) => void; // Changed to pass full profile
|
||||
@@ -62,7 +62,7 @@ export function ProfileQuickSelect({
|
||||
const isProfileSelected = (profile: AIProfile): boolean => {
|
||||
if (profile.provider === 'cursor') {
|
||||
// For cursor profiles, check if cursor model matches
|
||||
const profileCursorModel = `cursor-${profile.cursorModel || 'auto'}`;
|
||||
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
|
||||
return selectedCursorModel === profileCursorModel;
|
||||
}
|
||||
// For Claude profiles
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useGithubIssues, useIssueValidation } from './github-issues-view/hooks'
|
||||
import { IssueRow, IssueDetailPanel, IssuesListHeader } from './github-issues-view/components';
|
||||
import { ValidationDialog } from './github-issues-view/dialogs';
|
||||
import { formatDate, getFeaturePriority } from './github-issues-view/utils';
|
||||
import { useModelOverride } from '@/components/shared';
|
||||
|
||||
export function GitHubIssuesView() {
|
||||
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||
@@ -21,6 +22,9 @@ export function GitHubIssuesView() {
|
||||
const { currentProject, defaultAIProfileId, aiProfiles, getCurrentWorktree, worktreesByProject } =
|
||||
useAppStore();
|
||||
|
||||
// Model override for validation
|
||||
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
||||
|
||||
const { openIssues, closedIssues, loading, refreshing, error, refresh } = useGithubIssues();
|
||||
|
||||
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
|
||||
@@ -85,6 +89,9 @@ export function GitHubIssuesView() {
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
// Use profile default model
|
||||
const featureModel = defaultProfile?.model ?? 'opus';
|
||||
|
||||
const feature = {
|
||||
id: `issue-${issue.number}-${crypto.randomUUID()}`,
|
||||
title: issue.title,
|
||||
@@ -93,7 +100,7 @@ export function GitHubIssuesView() {
|
||||
status: 'backlog' as const,
|
||||
passes: false,
|
||||
priority: getFeaturePriority(validation.estimatedComplexity),
|
||||
model: defaultProfile?.model ?? 'opus',
|
||||
model: featureModel,
|
||||
thinkingLevel: defaultProfile?.thinkingLevel ?? 'none',
|
||||
branchName: currentBranch,
|
||||
createdAt: new Date().toISOString(),
|
||||
@@ -205,6 +212,7 @@ export function GitHubIssuesView() {
|
||||
onClose={() => setSelectedIssue(null)}
|
||||
onShowRevalidateConfirm={() => setShowRevalidateConfirm(true)}
|
||||
formatDate={formatDate}
|
||||
modelOverride={validationModelOverride}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -228,7 +236,10 @@ export function GitHubIssuesView() {
|
||||
confirmText="Re-validate"
|
||||
onConfirm={() => {
|
||||
if (selectedIssue) {
|
||||
handleValidateIssue(selectedIssue, { forceRevalidate: true });
|
||||
handleValidateIssue(selectedIssue, {
|
||||
forceRevalidate: true,
|
||||
model: validationModelOverride.effectiveModel,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { IssueDetailPanelProps } from '../types';
|
||||
import { isValidationStale } from '../utils';
|
||||
import { ModelOverrideTrigger } from '@/components/shared';
|
||||
|
||||
export function IssueDetailPanel({
|
||||
issue,
|
||||
@@ -27,6 +28,7 @@ export function IssueDetailPanel({
|
||||
onClose,
|
||||
onShowRevalidateConfirm,
|
||||
formatDate,
|
||||
modelOverride,
|
||||
}: IssueDetailPanelProps) {
|
||||
const isValidating = validatingIssues.has(issue.number);
|
||||
const cached = cachedValidations.get(issue.number);
|
||||
@@ -83,10 +85,23 @@ export function IssueDetailPanel({
|
||||
<Clock className="h-4 w-4 mr-1 text-yellow-500" />
|
||||
View (stale)
|
||||
</Button>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={modelOverride.effectiveModel}
|
||||
onModelChange={modelOverride.setOverride}
|
||||
phase="validationModel"
|
||||
isOverridden={modelOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, { forceRevalidate: true })}
|
||||
onClick={() =>
|
||||
onValidateIssue(issue, {
|
||||
forceRevalidate: true,
|
||||
model: modelOverride.effectiveModel,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Re-validate
|
||||
@@ -96,10 +111,24 @@ export function IssueDetailPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="default" size="sm" onClick={() => onValidateIssue(issue)}>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Validate with AI
|
||||
</Button>
|
||||
<>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={modelOverride.effectiveModel}
|
||||
onModelChange={modelOverride.setOverride}
|
||||
phase="validationModel"
|
||||
isOverridden={modelOverride.isOverridden}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onValidateIssue(issue, { model: modelOverride.effectiveModel })}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-1" />
|
||||
Validate with AI
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenInGitHub(issue.url)}>
|
||||
|
||||
@@ -205,8 +205,8 @@ export function useIssueValidation({
|
||||
}, []);
|
||||
|
||||
const handleValidateIssue = useCallback(
|
||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean } = {}) => {
|
||||
const { forceRevalidate = false } = options;
|
||||
async (issue: GitHubIssue, options: { forceRevalidate?: boolean; model?: string } = {}) => {
|
||||
const { forceRevalidate = false, model } = options;
|
||||
|
||||
if (!currentProject?.path) {
|
||||
toast.error('No project selected');
|
||||
@@ -233,6 +233,9 @@ export function useIssueValidation({
|
||||
description: 'You will be notified when the analysis is complete',
|
||||
});
|
||||
|
||||
// Use provided model override or fall back to global validationModel
|
||||
const modelToUse = model || validationModel;
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (api.github?.validateIssue) {
|
||||
@@ -244,7 +247,7 @@ export function useIssueValidation({
|
||||
issueBody: issue.body || '',
|
||||
issueLabels: issue.labels.map((l) => l.name),
|
||||
},
|
||||
validationModel
|
||||
modelToUse
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { GitHubIssue, StoredValidation } from '@/lib/electron';
|
||||
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
||||
|
||||
export interface IssueRowProps {
|
||||
issue: GitHubIssue;
|
||||
@@ -18,11 +19,21 @@ export interface IssueDetailPanelProps {
|
||||
cachedValidations: Map<number, StoredValidation>;
|
||||
onValidateIssue: (
|
||||
issue: GitHubIssue,
|
||||
options?: { showDialog?: boolean; forceRevalidate?: boolean }
|
||||
options?: {
|
||||
showDialog?: boolean;
|
||||
forceRevalidate?: boolean;
|
||||
model?: ModelAlias | CursorModelId;
|
||||
}
|
||||
) => Promise<void>;
|
||||
onViewCachedValidation: (issue: GitHubIssue) => Promise<void>;
|
||||
onOpenInGitHub: (url: string) => void;
|
||||
onClose: () => void;
|
||||
onShowRevalidateConfirm: () => void;
|
||||
formatDate: (date: string) => string;
|
||||
/** Model override state */
|
||||
modelOverride: {
|
||||
effectiveModel: ModelAlias | CursorModelId;
|
||||
isOverridden: boolean;
|
||||
setOverride: (model: ModelAlias | CursorModelId | null) => void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Brain, Bot, Terminal } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type {
|
||||
AIProfile,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
ThinkingLevel,
|
||||
ModelProvider,
|
||||
CursorModelId,
|
||||
@@ -42,7 +42,7 @@ export function ProfileForm({
|
||||
description: profile.description || '',
|
||||
provider: (profile.provider || 'claude') as ModelProvider,
|
||||
// Claude-specific
|
||||
model: profile.model || ('sonnet' as AgentModel),
|
||||
model: profile.model || ('sonnet' as ModelAlias),
|
||||
thinkingLevel: profile.thinkingLevel || ('none' as ThinkingLevel),
|
||||
// Cursor-specific
|
||||
cursorModel: profile.cursorModel || ('auto' as CursorModelId),
|
||||
@@ -62,7 +62,7 @@ export function ProfileForm({
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelChange = (model: AgentModel) => {
|
||||
const handleModelChange = (model: ModelAlias) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
model,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Brain, Zap, Scale, Cpu, Rocket, Sparkles } from 'lucide-react';
|
||||
import type { AgentModel, ThinkingLevel } from '@/store/app-store';
|
||||
import type { ModelAlias, ThinkingLevel } from '@/store/app-store';
|
||||
|
||||
// Icon mapping for profiles
|
||||
export const PROFILE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
@@ -22,7 +22,7 @@ export const ICON_OPTIONS = [
|
||||
];
|
||||
|
||||
// Model options for the form
|
||||
export const CLAUDE_MODELS: { id: AgentModel; label: string }[] = [
|
||||
export const CLAUDE_MODELS: { id: ModelAlias; label: string }[] = [
|
||||
{ id: 'haiku', label: 'Claude Haiku' },
|
||||
{ id: 'sonnet', label: 'Claude Sonnet' },
|
||||
{ id: 'opus', label: 'Claude Opus' },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentModel, ModelProvider, AIProfile } from '@automaker/types';
|
||||
import type { ModelAlias, ModelProvider, AIProfile } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
// Helper to determine provider from model (legacy, always returns 'claude')
|
||||
export function getProviderFromModel(model: AgentModel): ModelProvider {
|
||||
export function getProviderFromModel(model: ModelAlias): ModelProvider {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import type { AIProfile } from '@/store/app-store';
|
||||
import type { AgentModel } from '@automaker/types';
|
||||
import type { ModelAlias } from '@automaker/types';
|
||||
|
||||
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||
|
||||
@@ -36,7 +36,7 @@ interface FeatureDefaultsSectionProps {
|
||||
defaultRequirePlanApproval: boolean;
|
||||
defaultAIProfileId: string | null;
|
||||
aiProfiles: AIProfile[];
|
||||
validationModel: AgentModel;
|
||||
validationModel: ModelAlias;
|
||||
onShowProfilesOnlyChange: (value: boolean) => void;
|
||||
onDefaultSkipTestsChange: (value: boolean) => void;
|
||||
onEnableDependencyBlockingChange: (value: boolean) => void;
|
||||
@@ -44,7 +44,7 @@ interface FeatureDefaultsSectionProps {
|
||||
onDefaultPlanningModeChange: (value: PlanningMode) => void;
|
||||
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
|
||||
onDefaultAIProfileIdChange: (value: string | null) => void;
|
||||
onValidationModelChange: (value: AgentModel) => void;
|
||||
onValidationModelChange: (value: ModelAlias) => void;
|
||||
}
|
||||
|
||||
export function FeatureDefaultsSection({
|
||||
@@ -243,7 +243,7 @@ export function FeatureDefaultsSection({
|
||||
<Label className="text-foreground font-medium">Issue Validation Model</Label>
|
||||
<Select
|
||||
value={validationModel}
|
||||
onValueChange={(v: string) => onValidationModelChange(v as AgentModel)}
|
||||
onValueChange={(v: string) => onValidationModelChange(v as ModelAlias)}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] h-8" data-testid="validation-model-select">
|
||||
<SelectValue />
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import type { AgentModel, CursorModelId } from '@automaker/types';
|
||||
import type { ModelAlias, CursorModelId } from '@automaker/types';
|
||||
import { stripProviderPrefix } from '@automaker/types';
|
||||
import { CLAUDE_MODELS, CURSOR_MODELS } from '@/components/views/board-view/shared/model-constants';
|
||||
|
||||
interface PhaseModelSelectorProps {
|
||||
label: string;
|
||||
description: string;
|
||||
value: AgentModel | CursorModelId;
|
||||
onChange: (model: AgentModel | CursorModelId) => void;
|
||||
value: ModelAlias | CursorModelId;
|
||||
onChange: (model: ModelAlias | CursorModelId) => void;
|
||||
}
|
||||
|
||||
export function PhaseModelSelector({
|
||||
@@ -20,13 +21,10 @@ export function PhaseModelSelector({
|
||||
|
||||
// Filter Cursor models to only show enabled ones
|
||||
const availableCursorModels = CURSOR_MODELS.filter((model) => {
|
||||
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
return enabledCursorModels.includes(cursorId);
|
||||
});
|
||||
|
||||
// Check if current value is a Claude model or Cursor model
|
||||
const isClaudeModel = (v: string) => ['haiku', 'sonnet', 'opus'].includes(v);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -50,7 +48,7 @@ export function PhaseModelSelector({
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => onChange(model.id as AgentModel)}
|
||||
onClick={() => onChange(model.id as ModelAlias)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-medium',
|
||||
'transition-all duration-150',
|
||||
@@ -75,7 +73,7 @@ export function PhaseModelSelector({
|
||||
|
||||
{/* Cursor Models */}
|
||||
{availableCursorModels.map((model) => {
|
||||
const cursorId = model.id.replace('cursor-', '') as CursorModelId;
|
||||
const cursorId = stripProviderPrefix(model.id) as CursorModelId;
|
||||
const isActive = value === cursorId;
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Terminal, Info } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { CursorModelId, CursorModelConfig } from '@automaker/types';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
@@ -33,6 +34,7 @@ export function CursorSettingsTab() {
|
||||
// Global settings from store
|
||||
const { enabledCursorModels, cursorDefaultModel, setCursorDefaultModel, toggleCursorModel } =
|
||||
useAppStore();
|
||||
const { setCursorCliStatus } = useSetupStore();
|
||||
|
||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -48,11 +50,24 @@ export function CursorSettingsTab() {
|
||||
const statusResult = await api.setup.getCursorStatus();
|
||||
|
||||
if (statusResult.success) {
|
||||
setStatus({
|
||||
const newStatus = {
|
||||
installed: statusResult.installed ?? false,
|
||||
version: statusResult.version ?? undefined,
|
||||
authenticated: statusResult.auth?.authenticated ?? false,
|
||||
method: statusResult.auth?.method,
|
||||
};
|
||||
setStatus(newStatus);
|
||||
|
||||
// Also update the global setup store so other components can access the status
|
||||
setCursorCliStatus({
|
||||
installed: newStatus.installed,
|
||||
version: newStatus.version,
|
||||
auth: newStatus.authenticated
|
||||
? {
|
||||
authenticated: true,
|
||||
method: newStatus.method || 'unknown',
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -61,7 +76,7 @@ export function CursorSettingsTab() {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [setCursorCliStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
46
apps/ui/src/hooks/use-cursor-status-init.ts
Normal file
46
apps/ui/src/hooks/use-cursor-status-init.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
|
||||
/**
|
||||
* Hook to initialize Cursor CLI status on app startup.
|
||||
* This ensures the cursorCliStatus is available in the setup store
|
||||
* before the user opens feature dialogs.
|
||||
*/
|
||||
export function useCursorStatusInit() {
|
||||
const { setCursorCliStatus, cursorCliStatus } = useSetupStore();
|
||||
const initialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only initialize once per session
|
||||
if (initialized.current || cursorCliStatus !== null) {
|
||||
return;
|
||||
}
|
||||
initialized.current = true;
|
||||
|
||||
const initCursorStatus = async () => {
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const statusResult = await api.setup.getCursorStatus();
|
||||
|
||||
if (statusResult.success) {
|
||||
setCursorCliStatus({
|
||||
installed: statusResult.installed ?? false,
|
||||
version: statusResult.version ?? undefined,
|
||||
auth: statusResult.auth?.authenticated
|
||||
? {
|
||||
authenticated: true,
|
||||
method: statusResult.auth.method || 'unknown',
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - cursor is optional
|
||||
console.debug('[CursorStatusInit] Failed to check cursor status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initCursorStatus();
|
||||
}, [setCursorCliStatus, cursorCliStatus]);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
IssueValidationResponse,
|
||||
IssueValidationEvent,
|
||||
StoredValidation,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
} from '@automaker/types';
|
||||
import { getJSON, setJSON, removeItem } from './storage';
|
||||
|
||||
@@ -198,7 +198,7 @@ export interface GitHubAPI {
|
||||
validateIssue: (
|
||||
projectPath: string,
|
||||
issue: IssueValidationInput,
|
||||
model?: AgentModel
|
||||
model?: ModelAlias
|
||||
) => Promise<{ success: boolean; message?: string; issueNumber?: number; error?: string }>;
|
||||
/** Check validation status for an issue or all issues */
|
||||
getValidationStatus: (
|
||||
@@ -2734,7 +2734,7 @@ function createMockGitHubAPI(): GitHubAPI {
|
||||
mergedPRs: [],
|
||||
};
|
||||
},
|
||||
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: AgentModel) => {
|
||||
validateIssue: async (projectPath: string, issue: IssueValidationInput, model?: ModelAlias) => {
|
||||
console.log('[Mock] Starting async validation:', { projectPath, issue, model });
|
||||
|
||||
// Simulate async validation in background
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { AgentModel } from '@/store/app-store';
|
||||
import type { ModelAlias } from '@/store/app-store';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
@@ -9,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
|
||||
/**
|
||||
* Determine if the current model supports extended thinking controls
|
||||
*/
|
||||
export function modelSupportsThinking(_model?: AgentModel | string): boolean {
|
||||
export function modelSupportsThinking(_model?: ModelAlias | string): boolean {
|
||||
// All Claude models support thinking
|
||||
return true;
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function modelSupportsThinking(_model?: AgentModel | string): boolean {
|
||||
/**
|
||||
* Get display name for a model
|
||||
*/
|
||||
export function getModelDisplayName(model: AgentModel | string): string {
|
||||
export function getModelDisplayName(model: ModelAlias | string): string {
|
||||
const displayNames: Record<string, string> = {
|
||||
haiku: 'Claude Haiku',
|
||||
sonnet: 'Claude Sonnet',
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Project, TrashedProject } from '@/lib/electron';
|
||||
import type {
|
||||
Feature as BaseFeature,
|
||||
FeatureImagePath,
|
||||
AgentModel,
|
||||
ModelAlias,
|
||||
PlanningMode,
|
||||
AIProfile,
|
||||
CursorModelId,
|
||||
@@ -13,8 +13,8 @@ import type {
|
||||
} from '@automaker/types';
|
||||
import { getAllCursorModelIds, DEFAULT_PHASE_MODELS } from '@automaker/types';
|
||||
|
||||
// Re-export ThemeMode for convenience
|
||||
export type { ThemeMode };
|
||||
// Re-export types for convenience
|
||||
export type { ThemeMode, ModelAlias };
|
||||
|
||||
export type ViewMode =
|
||||
| 'welcome'
|
||||
@@ -477,10 +477,10 @@ export interface AppState {
|
||||
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
||||
|
||||
// Enhancement Model Settings
|
||||
enhancementModel: AgentModel; // Model used for feature enhancement (default: sonnet)
|
||||
enhancementModel: ModelAlias; // Model used for feature enhancement (default: sonnet)
|
||||
|
||||
// Validation Model Settings
|
||||
validationModel: AgentModel; // Model used for GitHub issue validation (default: opus)
|
||||
validationModel: ModelAlias; // Model used for GitHub issue validation (default: opus)
|
||||
|
||||
// Phase Model Settings - per-phase AI model configuration
|
||||
phaseModels: PhaseModelConfig;
|
||||
@@ -760,13 +760,13 @@ export interface AppActions {
|
||||
setMuteDoneSound: (muted: boolean) => void;
|
||||
|
||||
// Enhancement Model actions
|
||||
setEnhancementModel: (model: AgentModel) => void;
|
||||
setEnhancementModel: (model: ModelAlias) => void;
|
||||
|
||||
// Validation Model actions
|
||||
setValidationModel: (model: AgentModel) => void;
|
||||
setValidationModel: (model: ModelAlias) => void;
|
||||
|
||||
// Phase Model actions
|
||||
setPhaseModel: (phase: PhaseModelKey, model: AgentModel | CursorModelId) => void;
|
||||
setPhaseModel: (phase: PhaseModelKey, model: ModelAlias | CursorModelId) => void;
|
||||
setPhaseModels: (models: Partial<PhaseModelConfig>) => void;
|
||||
resetPhaseModels: () => void;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user