mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge pull request #650 from AutoMaker-Org/fix/ideation-view-non-claude-models
fix: ideation view not working with other providers
This commit is contained in:
@@ -43,7 +43,6 @@ import { createEnhancePromptRoutes } from './routes/enhance-prompt/index.js';
|
|||||||
import { createWorktreeRoutes } from './routes/worktree/index.js';
|
import { createWorktreeRoutes } from './routes/worktree/index.js';
|
||||||
import { createGitRoutes } from './routes/git/index.js';
|
import { createGitRoutes } from './routes/git/index.js';
|
||||||
import { createSetupRoutes } from './routes/setup/index.js';
|
import { createSetupRoutes } from './routes/setup/index.js';
|
||||||
import { createSuggestionsRoutes } from './routes/suggestions/index.js';
|
|
||||||
import { createModelsRoutes } from './routes/models/index.js';
|
import { createModelsRoutes } from './routes/models/index.js';
|
||||||
import { createRunningAgentsRoutes } from './routes/running-agents/index.js';
|
import { createRunningAgentsRoutes } from './routes/running-agents/index.js';
|
||||||
import { createWorkspaceRoutes } from './routes/workspace/index.js';
|
import { createWorkspaceRoutes } from './routes/workspace/index.js';
|
||||||
@@ -331,7 +330,6 @@ app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
|
|||||||
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
|
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
|
||||||
app.use('/api/worktree', createWorktreeRoutes(events, settingsService));
|
app.use('/api/worktree', createWorktreeRoutes(events, settingsService));
|
||||||
app.use('/api/git', createGitRoutes());
|
app.use('/api/git', createGitRoutes());
|
||||||
app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService));
|
|
||||||
app.use('/api/models', createModelsRoutes());
|
app.use('/api/models', createModelsRoutes());
|
||||||
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
|
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
|
||||||
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
|
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
|
||||||
|
|||||||
@@ -337,10 +337,11 @@ export class CursorProvider extends CliProvider {
|
|||||||
'--stream-partial-output' // Real-time streaming
|
'--stream-partial-output' // Real-time streaming
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only add --force if NOT in read-only mode
|
// In read-only mode, use --mode ask for Q&A style (no tools)
|
||||||
// Without --force, Cursor CLI suggests changes but doesn't apply them
|
// Otherwise, add --force to allow file edits
|
||||||
// With --force, Cursor CLI can actually edit files
|
if (options.readOnly) {
|
||||||
if (!options.readOnly) {
|
cliArgs.push('--mode', 'ask');
|
||||||
|
} else {
|
||||||
cliArgs.push('--force');
|
cliArgs.push('--force');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,10 +673,13 @@ export class CursorProvider extends CliProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract prompt text to pass via stdin (avoids shell escaping issues)
|
// Embed system prompt into user prompt (Cursor CLI doesn't support separate system messages)
|
||||||
const promptText = this.extractPromptText(options);
|
const effectiveOptions = this.embedSystemPromptIntoPrompt(options);
|
||||||
|
|
||||||
const cliArgs = this.buildCliArgs(options);
|
// Extract prompt text to pass via stdin (avoids shell escaping issues)
|
||||||
|
const promptText = this.extractPromptText(effectiveOptions);
|
||||||
|
|
||||||
|
const cliArgs = this.buildCliArgs(effectiveOptions);
|
||||||
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
|
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
|
||||||
|
|
||||||
// Pass prompt via stdin to avoid shell interpretation of special characters
|
// Pass prompt via stdin to avoid shell interpretation of special characters
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Common utilities and state for suggestions routes
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { createLogger } from '@automaker/utils';
|
|
||||||
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
|
||||||
|
|
||||||
const logger = createLogger('Suggestions');
|
|
||||||
|
|
||||||
// Shared state for tracking generation status - private
|
|
||||||
let isRunning = false;
|
|
||||||
let currentAbortController: AbortController | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current running state
|
|
||||||
*/
|
|
||||||
export function getSuggestionsStatus(): {
|
|
||||||
isRunning: boolean;
|
|
||||||
currentAbortController: AbortController | null;
|
|
||||||
} {
|
|
||||||
return { isRunning, currentAbortController };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the running state and abort controller
|
|
||||||
*/
|
|
||||||
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
|
|
||||||
isRunning = running;
|
|
||||||
currentAbortController = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-export shared utilities
|
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
|
||||||
export const logError = createLogError(logger);
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
/**
|
|
||||||
* Business logic for generating suggestions
|
|
||||||
*
|
|
||||||
* Model is configurable via phaseModels.suggestionsModel in settings
|
|
||||||
* (AI Suggestions in the UI). Supports both Claude and Cursor models.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
|
||||||
import { DEFAULT_PHASE_MODELS, isCursorModel, type ThinkingLevel } from '@automaker/types';
|
|
||||||
import { resolvePhaseModel } from '@automaker/model-resolver';
|
|
||||||
import { extractJsonWithArray } from '../../lib/json-extractor.js';
|
|
||||||
import { streamingQuery } from '../../providers/simple-query-service.js';
|
|
||||||
import { FeatureLoader } from '../../services/feature-loader.js';
|
|
||||||
import { getAppSpecPath } from '@automaker/platform';
|
|
||||||
import * as secureFs from '../../lib/secure-fs.js';
|
|
||||||
import type { SettingsService } from '../../services/settings-service.js';
|
|
||||||
import {
|
|
||||||
getAutoLoadClaudeMdSetting,
|
|
||||||
getPromptCustomization,
|
|
||||||
getPhaseModelWithOverrides,
|
|
||||||
getProviderByModelId,
|
|
||||||
} from '../../lib/settings-helpers.js';
|
|
||||||
|
|
||||||
const logger = createLogger('Suggestions');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract implemented features from app_spec.txt XML content
|
|
||||||
*
|
|
||||||
* Note: This uses regex-based parsing which is sufficient for our controlled
|
|
||||||
* XML structure. If more complex XML parsing is needed in the future, consider
|
|
||||||
* using a library like 'fast-xml-parser' or 'xml2js'.
|
|
||||||
*/
|
|
||||||
function extractImplementedFeatures(specContent: string): string[] {
|
|
||||||
const features: string[] = [];
|
|
||||||
|
|
||||||
// Match <implemented_features>...</implemented_features> section
|
|
||||||
const implementedMatch = specContent.match(
|
|
||||||
/<implemented_features>([\s\S]*?)<\/implemented_features>/
|
|
||||||
);
|
|
||||||
|
|
||||||
if (implementedMatch) {
|
|
||||||
const implementedSection = implementedMatch[1];
|
|
||||||
|
|
||||||
// Extract feature names from <name>...</name> tags using matchAll
|
|
||||||
const nameRegex = /<name>(.*?)<\/name>/g;
|
|
||||||
const matches = implementedSection.matchAll(nameRegex);
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
features.push(match[1].trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load existing context (app spec and backlog features) to avoid duplicates
|
|
||||||
*/
|
|
||||||
async function loadExistingContext(projectPath: string): Promise<string> {
|
|
||||||
let context = '';
|
|
||||||
|
|
||||||
// 1. Read app_spec.txt for implemented features
|
|
||||||
try {
|
|
||||||
const appSpecPath = getAppSpecPath(projectPath);
|
|
||||||
const specContent = (await secureFs.readFile(appSpecPath, 'utf-8')) as string;
|
|
||||||
|
|
||||||
if (specContent && specContent.trim().length > 0) {
|
|
||||||
const implementedFeatures = extractImplementedFeatures(specContent);
|
|
||||||
|
|
||||||
if (implementedFeatures.length > 0) {
|
|
||||||
context += '\n\n=== ALREADY IMPLEMENTED FEATURES ===\n';
|
|
||||||
context += 'These features are already implemented in the codebase:\n';
|
|
||||||
context += implementedFeatures.map((feature) => `- ${feature}`).join('\n') + '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// app_spec.txt doesn't exist or can't be read - that's okay
|
|
||||||
logger.debug('No app_spec.txt found or error reading it:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Load existing features from backlog
|
|
||||||
try {
|
|
||||||
const featureLoader = new FeatureLoader();
|
|
||||||
const features = await featureLoader.getAll(projectPath);
|
|
||||||
|
|
||||||
if (features.length > 0) {
|
|
||||||
context += '\n\n=== EXISTING FEATURES IN BACKLOG ===\n';
|
|
||||||
context += 'These features are already planned or in progress:\n';
|
|
||||||
context +=
|
|
||||||
features
|
|
||||||
.map((feature) => {
|
|
||||||
const status = feature.status || 'pending';
|
|
||||||
const title = feature.title || feature.description?.substring(0, 50) || 'Untitled';
|
|
||||||
return `- ${title} (${status})`;
|
|
||||||
})
|
|
||||||
.join('\n') + '\n';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Features directory doesn't exist or can't be read - that's okay
|
|
||||||
logger.debug('No features found or error loading them:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Schema for suggestions output
|
|
||||||
*/
|
|
||||||
const suggestionsSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
suggestions: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
id: { type: 'string' },
|
|
||||||
category: { type: 'string' },
|
|
||||||
description: { type: 'string' },
|
|
||||||
priority: {
|
|
||||||
type: 'number',
|
|
||||||
minimum: 1,
|
|
||||||
maximum: 3,
|
|
||||||
},
|
|
||||||
reasoning: { type: 'string' },
|
|
||||||
},
|
|
||||||
required: ['category', 'description', 'priority', 'reasoning'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['suggestions'],
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function generateSuggestions(
|
|
||||||
projectPath: string,
|
|
||||||
suggestionType: string,
|
|
||||||
events: EventEmitter,
|
|
||||||
abortController: AbortController,
|
|
||||||
settingsService?: SettingsService,
|
|
||||||
modelOverride?: string,
|
|
||||||
thinkingLevelOverride?: ThinkingLevel
|
|
||||||
): Promise<void> {
|
|
||||||
// Get customized prompts from settings
|
|
||||||
const prompts = await getPromptCustomization(settingsService, '[Suggestions]');
|
|
||||||
|
|
||||||
// Map suggestion types to their prompts
|
|
||||||
const typePrompts: Record<string, string> = {
|
|
||||||
features: prompts.suggestions.featuresPrompt,
|
|
||||||
refactoring: prompts.suggestions.refactoringPrompt,
|
|
||||||
security: prompts.suggestions.securityPrompt,
|
|
||||||
performance: prompts.suggestions.performancePrompt,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load existing context to avoid duplicates
|
|
||||||
const existingContext = await loadExistingContext(projectPath);
|
|
||||||
|
|
||||||
const prompt = `${typePrompts[suggestionType] || typePrompts.features}
|
|
||||||
${existingContext}
|
|
||||||
|
|
||||||
${existingContext ? '\nIMPORTANT: Do NOT suggest features that are already implemented or already in the backlog above. Focus on NEW ideas that complement what already exists.\n' : ''}
|
|
||||||
${prompts.suggestions.baseTemplate}`;
|
|
||||||
|
|
||||||
// Don't send initial message - let the agent output speak for itself
|
|
||||||
// The first agent message will be captured as an info entry
|
|
||||||
|
|
||||||
// Load autoLoadClaudeMd setting
|
|
||||||
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
|
|
||||||
projectPath,
|
|
||||||
settingsService,
|
|
||||||
'[Suggestions]'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get model from phase settings with provider info (AI Suggestions = suggestionsModel)
|
|
||||||
// Use override if provided, otherwise fall back to settings
|
|
||||||
let model: string;
|
|
||||||
let thinkingLevel: ThinkingLevel | undefined;
|
|
||||||
let provider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
|
|
||||||
let credentials: import('@automaker/types').Credentials | undefined;
|
|
||||||
|
|
||||||
if (modelOverride) {
|
|
||||||
// Use explicit override - resolve the model string
|
|
||||||
const resolved = resolvePhaseModel({
|
|
||||||
model: modelOverride,
|
|
||||||
thinkingLevel: thinkingLevelOverride,
|
|
||||||
});
|
|
||||||
model = resolved.model;
|
|
||||||
thinkingLevel = resolved.thinkingLevel;
|
|
||||||
|
|
||||||
// Try to find a provider for this model (e.g., GLM, MiniMax models)
|
|
||||||
if (settingsService) {
|
|
||||||
const providerResult = await getProviderByModelId(
|
|
||||||
modelOverride,
|
|
||||||
settingsService,
|
|
||||||
'[Suggestions]'
|
|
||||||
);
|
|
||||||
provider = providerResult.provider;
|
|
||||||
// Use resolved model from provider if available (maps to Claude model)
|
|
||||||
if (providerResult.resolvedModel) {
|
|
||||||
model = providerResult.resolvedModel;
|
|
||||||
}
|
|
||||||
credentials = providerResult.credentials ?? (await settingsService.getCredentials());
|
|
||||||
}
|
|
||||||
// If no settingsService, credentials remains undefined (initialized above)
|
|
||||||
} else if (settingsService) {
|
|
||||||
// Use settings-based model with provider info
|
|
||||||
const phaseResult = await getPhaseModelWithOverrides(
|
|
||||||
'suggestionsModel',
|
|
||||||
settingsService,
|
|
||||||
projectPath,
|
|
||||||
'[Suggestions]'
|
|
||||||
);
|
|
||||||
const resolved = resolvePhaseModel(phaseResult.phaseModel);
|
|
||||||
model = resolved.model;
|
|
||||||
thinkingLevel = resolved.thinkingLevel;
|
|
||||||
provider = phaseResult.provider;
|
|
||||||
credentials = phaseResult.credentials;
|
|
||||||
} else {
|
|
||||||
// Fallback to defaults
|
|
||||||
const resolved = resolvePhaseModel(DEFAULT_PHASE_MODELS.suggestionsModel);
|
|
||||||
model = resolved.model;
|
|
||||||
thinkingLevel = resolved.thinkingLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'[Suggestions] Using model:',
|
|
||||||
model,
|
|
||||||
provider ? `via provider: ${provider.name}` : 'direct API'
|
|
||||||
);
|
|
||||||
|
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
// Determine if we should use structured output (Claude supports it, Cursor doesn't)
|
|
||||||
const useStructuredOutput = !isCursorModel(model);
|
|
||||||
|
|
||||||
// Build the final prompt - for Cursor, include JSON schema instructions
|
|
||||||
let finalPrompt = prompt;
|
|
||||||
if (!useStructuredOutput) {
|
|
||||||
finalPrompt = `${prompt}
|
|
||||||
|
|
||||||
CRITICAL INSTRUCTIONS:
|
|
||||||
1. DO NOT write any files. Return the JSON in your response only.
|
|
||||||
2. After analyzing the project, respond with ONLY a JSON object - no explanations, no markdown, just raw JSON.
|
|
||||||
3. The JSON must match this exact schema:
|
|
||||||
|
|
||||||
${JSON.stringify(suggestionsSchema, null, 2)}
|
|
||||||
|
|
||||||
Your entire response should be valid JSON starting with { and ending with }. No text before or after.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use streamingQuery with event callbacks
|
|
||||||
const result = await streamingQuery({
|
|
||||||
prompt: finalPrompt,
|
|
||||||
model,
|
|
||||||
cwd: projectPath,
|
|
||||||
maxTurns: 250,
|
|
||||||
allowedTools: ['Read', 'Glob', 'Grep'],
|
|
||||||
abortController,
|
|
||||||
thinkingLevel,
|
|
||||||
readOnly: true, // Suggestions only reads code, doesn't write
|
|
||||||
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
|
|
||||||
claudeCompatibleProvider: provider, // Pass provider for alternative endpoint configuration
|
|
||||||
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
|
||||||
outputFormat: useStructuredOutput
|
|
||||||
? {
|
|
||||||
type: 'json_schema',
|
|
||||||
schema: suggestionsSchema,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
onText: (text) => {
|
|
||||||
responseText += text;
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_progress',
|
|
||||||
content: text,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onToolUse: (tool, input) => {
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_tool',
|
|
||||||
tool,
|
|
||||||
input,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use structured output if available, otherwise fall back to parsing text
|
|
||||||
try {
|
|
||||||
let structuredOutput: { suggestions: Array<Record<string, unknown>> } | null = null;
|
|
||||||
|
|
||||||
if (result.structured_output) {
|
|
||||||
structuredOutput = result.structured_output as {
|
|
||||||
suggestions: Array<Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
logger.debug('Received structured output:', structuredOutput);
|
|
||||||
} else if (responseText) {
|
|
||||||
// Fallback: try to parse from text using shared extraction utility
|
|
||||||
logger.warn('No structured output received, attempting to parse from text');
|
|
||||||
structuredOutput = extractJsonWithArray<{ suggestions: Array<Record<string, unknown>> }>(
|
|
||||||
responseText,
|
|
||||||
'suggestions',
|
|
||||||
{ logger }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (structuredOutput && structuredOutput.suggestions) {
|
|
||||||
// Use structured output directly
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_complete',
|
|
||||||
suggestions: structuredOutput.suggestions.map((s: Record<string, unknown>, i: number) => ({
|
|
||||||
...s,
|
|
||||||
id: s.id || `suggestion-${Date.now()}-${i}`,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('No valid JSON found in response');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Log the parsing error for debugging
|
|
||||||
logger.error('Failed to parse suggestions JSON from AI response:', error);
|
|
||||||
// Return generic suggestions if parsing fails
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_complete',
|
|
||||||
suggestions: [
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
|
||||||
category: 'Analysis',
|
|
||||||
description: 'Review the AI analysis output for insights',
|
|
||||||
priority: 1,
|
|
||||||
reasoning: 'The AI provided analysis but suggestions need manual review',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* Suggestions routes - HTTP API for AI-powered feature suggestions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Router } from 'express';
|
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
|
||||||
import { validatePathParams } from '../../middleware/validate-paths.js';
|
|
||||||
import { createGenerateHandler } from './routes/generate.js';
|
|
||||||
import { createStopHandler } from './routes/stop.js';
|
|
||||||
import { createStatusHandler } from './routes/status.js';
|
|
||||||
import type { SettingsService } from '../../services/settings-service.js';
|
|
||||||
|
|
||||||
export function createSuggestionsRoutes(
|
|
||||||
events: EventEmitter,
|
|
||||||
settingsService?: SettingsService
|
|
||||||
): Router {
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/generate',
|
|
||||||
validatePathParams('projectPath'),
|
|
||||||
createGenerateHandler(events, settingsService)
|
|
||||||
);
|
|
||||||
router.post('/stop', createStopHandler());
|
|
||||||
router.get('/status', createStatusHandler());
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/**
|
|
||||||
* POST /generate endpoint - Generate suggestions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import type { EventEmitter } from '../../../lib/events.js';
|
|
||||||
import { createLogger } from '@automaker/utils';
|
|
||||||
import type { ThinkingLevel } from '@automaker/types';
|
|
||||||
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
|
|
||||||
import { generateSuggestions } from '../generate-suggestions.js';
|
|
||||||
import type { SettingsService } from '../../../services/settings-service.js';
|
|
||||||
|
|
||||||
const logger = createLogger('Suggestions');
|
|
||||||
|
|
||||||
export function createGenerateHandler(events: EventEmitter, settingsService?: SettingsService) {
|
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
projectPath,
|
|
||||||
suggestionType = 'features',
|
|
||||||
model,
|
|
||||||
thinkingLevel,
|
|
||||||
} = req.body as {
|
|
||||||
projectPath: string;
|
|
||||||
suggestionType?: string;
|
|
||||||
model?: string;
|
|
||||||
thinkingLevel?: ThinkingLevel;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!projectPath) {
|
|
||||||
res.status(400).json({ success: false, error: 'projectPath required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isRunning } = getSuggestionsStatus();
|
|
||||||
if (isRunning) {
|
|
||||||
res.json({
|
|
||||||
success: false,
|
|
||||||
error: 'Suggestions generation is already running',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRunningState(true);
|
|
||||||
const abortController = new AbortController();
|
|
||||||
setRunningState(true, abortController);
|
|
||||||
|
|
||||||
// Start generation in background
|
|
||||||
generateSuggestions(
|
|
||||||
projectPath,
|
|
||||||
suggestionType,
|
|
||||||
events,
|
|
||||||
abortController,
|
|
||||||
settingsService,
|
|
||||||
model,
|
|
||||||
thinkingLevel
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
logError(error, 'Generate suggestions failed (background)');
|
|
||||||
events.emit('suggestions:event', {
|
|
||||||
type: 'suggestions_error',
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setRunningState(false, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, 'Generate suggestions failed');
|
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* GET /status endpoint - Get status
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import { getSuggestionsStatus, getErrorMessage, logError } from '../common.js';
|
|
||||||
|
|
||||||
export function createStatusHandler() {
|
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { isRunning } = getSuggestionsStatus();
|
|
||||||
res.json({ success: true, isRunning });
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, 'Get status failed');
|
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* POST /stop endpoint - Stop suggestions generation
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Request, Response } from 'express';
|
|
||||||
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
|
|
||||||
|
|
||||||
export function createStopHandler() {
|
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const { currentAbortController } = getSuggestionsStatus();
|
|
||||||
if (currentAbortController) {
|
|
||||||
currentAbortController.abort();
|
|
||||||
}
|
|
||||||
setRunningState(false, null);
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
logError(error, 'Stop suggestions failed');
|
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -39,9 +39,13 @@ import { ProviderFactory } from '../providers/provider-factory.js';
|
|||||||
import type { SettingsService } from './settings-service.js';
|
import type { SettingsService } from './settings-service.js';
|
||||||
import type { FeatureLoader } from './feature-loader.js';
|
import type { FeatureLoader } from './feature-loader.js';
|
||||||
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
import { resolveModelString } from '@automaker/model-resolver';
|
import { resolveModelString, resolvePhaseModel } from '@automaker/model-resolver';
|
||||||
import { stripProviderPrefix } from '@automaker/types';
|
import { stripProviderPrefix } from '@automaker/types';
|
||||||
import { getPromptCustomization, getProviderByModelId } from '../lib/settings-helpers.js';
|
import {
|
||||||
|
getPromptCustomization,
|
||||||
|
getProviderByModelId,
|
||||||
|
getPhaseModelWithOverrides,
|
||||||
|
} from '../lib/settings-helpers.js';
|
||||||
|
|
||||||
const logger = createLogger('IdeationService');
|
const logger = createLogger('IdeationService');
|
||||||
|
|
||||||
@@ -684,8 +688,24 @@ export class IdeationService {
|
|||||||
existingWorkContext
|
existingWorkContext
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve model alias to canonical identifier (with prefix)
|
// Get model from phase settings with provider info (ideationModel)
|
||||||
const modelId = resolveModelString('sonnet');
|
const phaseResult = await getPhaseModelWithOverrides(
|
||||||
|
'ideationModel',
|
||||||
|
this.settingsService,
|
||||||
|
projectPath,
|
||||||
|
'[IdeationService]'
|
||||||
|
);
|
||||||
|
const resolved = resolvePhaseModel(phaseResult.phaseModel);
|
||||||
|
// resolvePhaseModel already resolves model aliases internally - no need to call resolveModelString again
|
||||||
|
const modelId = resolved.model;
|
||||||
|
const claudeCompatibleProvider = phaseResult.provider;
|
||||||
|
const credentials = phaseResult.credentials;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'generateSuggestions using model:',
|
||||||
|
modelId,
|
||||||
|
claudeCompatibleProvider ? `via provider: ${claudeCompatibleProvider.name}` : 'direct API'
|
||||||
|
);
|
||||||
|
|
||||||
// Create SDK options
|
// Create SDK options
|
||||||
const sdkOptions = createChatOptions({
|
const sdkOptions = createChatOptions({
|
||||||
@@ -700,9 +720,6 @@ export class IdeationService {
|
|||||||
// Strip provider prefix - providers need bare model IDs
|
// Strip provider prefix - providers need bare model IDs
|
||||||
const bareModel = stripProviderPrefix(modelId);
|
const bareModel = stripProviderPrefix(modelId);
|
||||||
|
|
||||||
// Get credentials for API calls (uses hardcoded model, no phase setting)
|
|
||||||
const credentials = await this.settingsService?.getCredentials();
|
|
||||||
|
|
||||||
const executeOptions: ExecuteOptions = {
|
const executeOptions: ExecuteOptions = {
|
||||||
prompt: prompt.prompt,
|
prompt: prompt.prompt,
|
||||||
model: bareModel,
|
model: bareModel,
|
||||||
@@ -713,6 +730,8 @@ export class IdeationService {
|
|||||||
// Disable all tools - we just want text generation, not codebase analysis
|
// Disable all tools - we just want text generation, not codebase analysis
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
|
readOnly: true, // Suggestions only need to return JSON, never write files
|
||||||
|
claudeCompatibleProvider, // Pass provider for alternative endpoint configuration
|
||||||
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
credentials, // Pass credentials for resolving 'credentials' apiKeySource
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const E2E_SETTINGS = {
|
|||||||
featureGenerationModel: { model: 'sonnet' },
|
featureGenerationModel: { model: 'sonnet' },
|
||||||
backlogPlanningModel: { model: 'sonnet' },
|
backlogPlanningModel: { model: 'sonnet' },
|
||||||
projectAnalysisModel: { model: 'sonnet' },
|
projectAnalysisModel: { model: 'sonnet' },
|
||||||
suggestionsModel: { model: 'sonnet' },
|
ideationModel: { model: 'sonnet' },
|
||||||
},
|
},
|
||||||
enhancementModel: 'sonnet',
|
enhancementModel: 'sonnet',
|
||||||
validationModel: 'opus',
|
validationModel: 'opus',
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useIdeationStore } from '@/store/ideation-store';
|
|||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useGenerateIdeationSuggestions } from '@/hooks/mutations';
|
import { useGenerateIdeationSuggestions } from '@/hooks/mutations';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
|
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
|
||||||
|
|
||||||
interface PromptListProps {
|
interface PromptListProps {
|
||||||
@@ -24,10 +23,8 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
|||||||
const generationJobs = useIdeationStore((s) => s.generationJobs);
|
const generationJobs = useIdeationStore((s) => s.generationJobs);
|
||||||
const setMode = useIdeationStore((s) => s.setMode);
|
const setMode = useIdeationStore((s) => s.setMode);
|
||||||
const addGenerationJob = useIdeationStore((s) => s.addGenerationJob);
|
const addGenerationJob = useIdeationStore((s) => s.addGenerationJob);
|
||||||
const updateJobStatus = useIdeationStore((s) => s.updateJobStatus);
|
|
||||||
const [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
|
const [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
|
||||||
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// React Query mutation
|
// React Query mutation
|
||||||
const generateMutation = useGenerateIdeationSuggestions(currentProject?.path ?? '');
|
const generateMutation = useGenerateIdeationSuggestions(currentProject?.path ?? '');
|
||||||
@@ -72,27 +69,13 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
|||||||
toast.info(`Generating ideas for "${prompt.title}"...`);
|
toast.info(`Generating ideas for "${prompt.title}"...`);
|
||||||
setMode('dashboard');
|
setMode('dashboard');
|
||||||
|
|
||||||
|
// Start mutation - onSuccess/onError are handled at the hook level to ensure
|
||||||
|
// they fire even after this component unmounts (which happens due to setMode above)
|
||||||
generateMutation.mutate(
|
generateMutation.mutate(
|
||||||
{ promptId: prompt.id, category },
|
{ promptId: prompt.id, category, jobId, promptTitle: prompt.title },
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
// Optional: reset local loading state if component is still mounted
|
||||||
updateJobStatus(jobId, 'ready', data.suggestions);
|
onSettled: () => {
|
||||||
toast.success(`Generated ${data.suggestions.length} ideas for "${prompt.title}"`, {
|
|
||||||
duration: 10000,
|
|
||||||
action: {
|
|
||||||
label: 'View Ideas',
|
|
||||||
onClick: () => {
|
|
||||||
setMode('dashboard');
|
|
||||||
navigate({ to: '/ideation' });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setLoadingPromptId(null);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to generate suggestions:', error);
|
|
||||||
updateJobStatus(jobId, 'error', undefined, error.message);
|
|
||||||
toast.error(error.message);
|
|
||||||
setLoadingPromptId(null);
|
setLoadingPromptId(null);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const PHASE_LABELS: Record<PhaseModelKey, string> = {
|
|||||||
featureGenerationModel: 'Feature Generation',
|
featureGenerationModel: 'Feature Generation',
|
||||||
backlogPlanningModel: 'Backlog Planning',
|
backlogPlanningModel: 'Backlog Planning',
|
||||||
projectAnalysisModel: 'Project Analysis',
|
projectAnalysisModel: 'Project Analysis',
|
||||||
suggestionsModel: 'AI Suggestions',
|
ideationModel: 'Ideation',
|
||||||
memoryExtractionModel: 'Memory Extraction',
|
memoryExtractionModel: 'Memory Extraction',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ const GENERATION_TASKS: PhaseConfig[] = [
|
|||||||
description: 'Analyzes project structure for suggestions',
|
description: 'Analyzes project structure for suggestions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'suggestionsModel',
|
key: 'ideationModel',
|
||||||
label: 'AI Suggestions',
|
label: 'Ideation',
|
||||||
description: 'Model for feature, refactoring, security, and performance suggestions',
|
description: 'Model for ideation view (generating AI suggestions)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const PHASE_LABELS: Record<PhaseModelKey, string> = {
|
|||||||
featureGenerationModel: 'Feature Generation',
|
featureGenerationModel: 'Feature Generation',
|
||||||
backlogPlanningModel: 'Backlog Planning',
|
backlogPlanningModel: 'Backlog Planning',
|
||||||
projectAnalysisModel: 'Project Analysis',
|
projectAnalysisModel: 'Project Analysis',
|
||||||
suggestionsModel: 'AI Suggestions',
|
ideationModel: 'Ideation',
|
||||||
memoryExtractionModel: 'Memory Extraction',
|
memoryExtractionModel: 'Memory Extraction',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ const GENERATION_TASKS: PhaseConfig[] = [
|
|||||||
description: 'Analyzes project structure for suggestions',
|
description: 'Analyzes project structure for suggestions',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'suggestionsModel',
|
key: 'ideationModel',
|
||||||
label: 'AI Suggestions',
|
label: 'Ideation',
|
||||||
description: 'Model for feature, refactoring, security, and performance suggestions',
|
description: 'Model for ideation view (generating AI suggestions)',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { queryKeys } from '@/lib/query-keys';
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { IdeaCategory, IdeaSuggestion } from '@automaker/types';
|
import type { IdeaCategory, AnalysisSuggestion } from '@automaker/types';
|
||||||
|
import { useIdeationStore } from '@/store/ideation-store';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Input for generating ideation suggestions
|
* Input for generating ideation suggestions
|
||||||
@@ -16,15 +17,23 @@ import type { IdeaCategory, IdeaSuggestion } from '@automaker/types';
|
|||||||
interface GenerateSuggestionsInput {
|
interface GenerateSuggestionsInput {
|
||||||
promptId: string;
|
promptId: string;
|
||||||
category: IdeaCategory;
|
category: IdeaCategory;
|
||||||
|
/** Job ID for tracking generation progress - used to update job status on completion */
|
||||||
|
jobId: string;
|
||||||
|
/** Prompt title for toast notifications */
|
||||||
|
promptTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result from generating suggestions
|
* Result from generating suggestions
|
||||||
*/
|
*/
|
||||||
interface GenerateSuggestionsResult {
|
interface GenerateSuggestionsResult {
|
||||||
suggestions: IdeaSuggestion[];
|
suggestions: AnalysisSuggestion[];
|
||||||
promptId: string;
|
promptId: string;
|
||||||
category: IdeaCategory;
|
category: IdeaCategory;
|
||||||
|
/** Job ID passed through for onSuccess handler */
|
||||||
|
jobId: string;
|
||||||
|
/** Prompt title passed through for toast notifications */
|
||||||
|
promptTitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +61,7 @@ export function useGenerateIdeationSuggestions(projectPath: string) {
|
|||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (input: GenerateSuggestionsInput): Promise<GenerateSuggestionsResult> => {
|
mutationFn: async (input: GenerateSuggestionsInput): Promise<GenerateSuggestionsResult> => {
|
||||||
const { promptId, category } = input;
|
const { promptId, category, jobId, promptTitle } = input;
|
||||||
|
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.ideation?.generateSuggestions) {
|
if (!api.ideation?.generateSuggestions) {
|
||||||
@@ -69,14 +78,33 @@ export function useGenerateIdeationSuggestions(projectPath: string) {
|
|||||||
suggestions: result.suggestions ?? [],
|
suggestions: result.suggestions ?? [],
|
||||||
promptId,
|
promptId,
|
||||||
category,
|
category,
|
||||||
|
jobId,
|
||||||
|
promptTitle,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
|
// Update job status in Zustand store - this runs even if the component unmounts
|
||||||
|
// Using getState() to access store directly without hooks (safe in callbacks)
|
||||||
|
const updateJobStatus = useIdeationStore.getState().updateJobStatus;
|
||||||
|
updateJobStatus(data.jobId, 'ready', data.suggestions);
|
||||||
|
|
||||||
|
// Show success toast
|
||||||
|
toast.success(`Generated ${data.suggestions.length} ideas for "${data.promptTitle}"`, {
|
||||||
|
duration: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
// Invalidate ideation ideas cache
|
// Invalidate ideation ideas cache
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: queryKeys.ideation.ideas(projectPath),
|
queryKey: queryKeys.ideation.ideas(projectPath),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Toast notifications are handled by the component since it has access to prompt title
|
onError: (error, variables) => {
|
||||||
|
// Update job status to error - this runs even if the component unmounts
|
||||||
|
const updateJobStatus = useIdeationStore.getState().updateJobStatus;
|
||||||
|
updateJobStatus(variables.jobId, 'error', undefined, error.message);
|
||||||
|
|
||||||
|
// Show error toast
|
||||||
|
toast.error(`Failed to generate ideas: ${error.message}`);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
|||||||
projectAnalysisModel: migratePhaseModelEntry(
|
projectAnalysisModel: migratePhaseModelEntry(
|
||||||
serverSettings.phaseModels.projectAnalysisModel
|
serverSettings.phaseModels.projectAnalysisModel
|
||||||
),
|
),
|
||||||
suggestionsModel: migratePhaseModelEntry(serverSettings.phaseModels.suggestionsModel),
|
ideationModel: migratePhaseModelEntry(serverSettings.phaseModels.ideationModel),
|
||||||
memoryExtractionModel: migratePhaseModelEntry(
|
memoryExtractionModel: migratePhaseModelEntry(
|
||||||
serverSettings.phaseModels.memoryExtractionModel
|
serverSettings.phaseModels.memoryExtractionModel
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -370,40 +370,6 @@ export interface GitHubAPI {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature Suggestions types
|
|
||||||
export interface FeatureSuggestion {
|
|
||||||
id: string;
|
|
||||||
category: string;
|
|
||||||
description: string;
|
|
||||||
priority: number;
|
|
||||||
reasoning: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SuggestionsEvent {
|
|
||||||
type: 'suggestions_progress' | 'suggestions_tool' | 'suggestions_complete' | 'suggestions_error';
|
|
||||||
content?: string;
|
|
||||||
tool?: string;
|
|
||||||
input?: unknown;
|
|
||||||
suggestions?: FeatureSuggestion[];
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SuggestionType = 'features' | 'refactoring' | 'security' | 'performance';
|
|
||||||
|
|
||||||
export interface SuggestionsAPI {
|
|
||||||
generate: (
|
|
||||||
projectPath: string,
|
|
||||||
suggestionType?: SuggestionType
|
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
|
||||||
stop: () => Promise<{ success: boolean; error?: string }>;
|
|
||||||
status: () => Promise<{
|
|
||||||
success: boolean;
|
|
||||||
isRunning?: boolean;
|
|
||||||
error?: string;
|
|
||||||
}>;
|
|
||||||
onEvent: (callback: (event: SuggestionsEvent) => void) => () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Spec Regeneration types
|
// Spec Regeneration types
|
||||||
export type SpecRegenerationEvent =
|
export type SpecRegenerationEvent =
|
||||||
| { type: 'spec_regeneration_progress'; content: string; projectPath: string }
|
| { type: 'spec_regeneration_progress'; content: string; projectPath: string }
|
||||||
@@ -702,7 +668,6 @@ export interface ElectronAPI {
|
|||||||
};
|
};
|
||||||
worktree?: WorktreeAPI;
|
worktree?: WorktreeAPI;
|
||||||
git?: GitAPI;
|
git?: GitAPI;
|
||||||
suggestions?: SuggestionsAPI;
|
|
||||||
specRegeneration?: SpecRegenerationAPI;
|
specRegeneration?: SpecRegenerationAPI;
|
||||||
autoMode?: AutoModeAPI;
|
autoMode?: AutoModeAPI;
|
||||||
features?: FeaturesAPI;
|
features?: FeaturesAPI;
|
||||||
@@ -1333,9 +1298,6 @@ const getMockElectronAPI = (): ElectronAPI => {
|
|||||||
// Mock Git API (for non-worktree operations)
|
// Mock Git API (for non-worktree operations)
|
||||||
git: createMockGitAPI(),
|
git: createMockGitAPI(),
|
||||||
|
|
||||||
// Mock Suggestions API
|
|
||||||
suggestions: createMockSuggestionsAPI(),
|
|
||||||
|
|
||||||
// Mock Spec Regeneration API
|
// Mock Spec Regeneration API
|
||||||
specRegeneration: createMockSpecRegenerationAPI(),
|
specRegeneration: createMockSpecRegenerationAPI(),
|
||||||
|
|
||||||
@@ -2605,226 +2567,6 @@ function delay(ms: number, featureId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock Suggestions state and implementation
|
|
||||||
let mockSuggestionsRunning = false;
|
|
||||||
let mockSuggestionsCallbacks: ((event: SuggestionsEvent) => void)[] = [];
|
|
||||||
let mockSuggestionsTimeout: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
function createMockSuggestionsAPI(): SuggestionsAPI {
|
|
||||||
return {
|
|
||||||
generate: async (projectPath: string, suggestionType: SuggestionType = 'features') => {
|
|
||||||
if (mockSuggestionsRunning) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Suggestions generation is already running',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
mockSuggestionsRunning = true;
|
|
||||||
console.log(`[Mock] Generating ${suggestionType} suggestions for: ${projectPath}`);
|
|
||||||
|
|
||||||
// Simulate async suggestion generation
|
|
||||||
simulateSuggestionsGeneration(suggestionType);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
|
|
||||||
stop: async () => {
|
|
||||||
mockSuggestionsRunning = false;
|
|
||||||
if (mockSuggestionsTimeout) {
|
|
||||||
clearTimeout(mockSuggestionsTimeout);
|
|
||||||
mockSuggestionsTimeout = null;
|
|
||||||
}
|
|
||||||
return { success: true };
|
|
||||||
},
|
|
||||||
|
|
||||||
status: async () => {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
isRunning: mockSuggestionsRunning,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onEvent: (callback: (event: SuggestionsEvent) => void) => {
|
|
||||||
mockSuggestionsCallbacks.push(callback);
|
|
||||||
return () => {
|
|
||||||
mockSuggestionsCallbacks = mockSuggestionsCallbacks.filter((cb) => cb !== callback);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function emitSuggestionsEvent(event: SuggestionsEvent) {
|
|
||||||
mockSuggestionsCallbacks.forEach((cb) => cb(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function simulateSuggestionsGeneration(suggestionType: SuggestionType = 'features') {
|
|
||||||
const typeLabels: Record<SuggestionType, string> = {
|
|
||||||
features: 'feature suggestions',
|
|
||||||
refactoring: 'refactoring opportunities',
|
|
||||||
security: 'security vulnerabilities',
|
|
||||||
performance: 'performance issues',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Emit progress events
|
|
||||||
emitSuggestionsEvent({
|
|
||||||
type: 'suggestions_progress',
|
|
||||||
content: `Starting project analysis for ${typeLabels[suggestionType]}...\n`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
if (!mockSuggestionsRunning) return;
|
|
||||||
|
|
||||||
emitSuggestionsEvent({
|
|
||||||
type: 'suggestions_tool',
|
|
||||||
tool: 'Glob',
|
|
||||||
input: { pattern: '**/*.{ts,tsx,js,jsx}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
if (!mockSuggestionsRunning) return;
|
|
||||||
|
|
||||||
emitSuggestionsEvent({
|
|
||||||
type: 'suggestions_progress',
|
|
||||||
content: 'Analyzing codebase structure...\n',
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
if (!mockSuggestionsRunning) return;
|
|
||||||
|
|
||||||
emitSuggestionsEvent({
|
|
||||||
type: 'suggestions_progress',
|
|
||||||
content: `Identifying ${typeLabels[suggestionType]}...\n`,
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
mockSuggestionsTimeout = setTimeout(resolve, 500);
|
|
||||||
});
|
|
||||||
if (!mockSuggestionsRunning) return;
|
|
||||||
|
|
||||||
// Generate mock suggestions based on type
|
|
||||||
let mockSuggestions: FeatureSuggestion[];
|
|
||||||
|
|
||||||
switch (suggestionType) {
|
|
||||||
case 'refactoring':
|
|
||||||
mockSuggestions = [
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
|
||||||
category: 'Code Smell',
|
|
||||||
description: 'Extract duplicate validation logic into reusable utility',
|
|
||||||
priority: 1,
|
|
||||||
reasoning: 'Reduces code duplication and improves maintainability',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-1`,
|
|
||||||
category: 'Complexity',
|
|
||||||
description: 'Break down large handleSubmit function into smaller functions',
|
|
||||||
priority: 2,
|
|
||||||
reasoning: 'Function is too long and handles multiple responsibilities',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-2`,
|
|
||||||
category: 'Architecture',
|
|
||||||
description: 'Move business logic out of React components into hooks',
|
|
||||||
priority: 3,
|
|
||||||
reasoning: 'Improves separation of concerns and testability',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'security':
|
|
||||||
mockSuggestions = [
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
|
||||||
category: 'High',
|
|
||||||
description: 'Sanitize user input before rendering to prevent XSS',
|
|
||||||
priority: 1,
|
|
||||||
reasoning: 'User input is rendered without proper sanitization',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-1`,
|
|
||||||
category: 'Medium',
|
|
||||||
description: 'Add rate limiting to authentication endpoints',
|
|
||||||
priority: 2,
|
|
||||||
reasoning: 'Prevents brute force attacks on authentication',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-2`,
|
|
||||||
category: 'Low',
|
|
||||||
description: 'Remove sensitive information from error messages',
|
|
||||||
priority: 3,
|
|
||||||
reasoning: 'Error messages may leak implementation details',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'performance':
|
|
||||||
mockSuggestions = [
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
|
||||||
category: 'Rendering',
|
|
||||||
description: 'Add React.memo to prevent unnecessary re-renders',
|
|
||||||
priority: 1,
|
|
||||||
reasoning: "Components re-render even when props haven't changed",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-1`,
|
|
||||||
category: 'Bundle Size',
|
|
||||||
description: 'Implement code splitting for route components',
|
|
||||||
priority: 2,
|
|
||||||
reasoning: 'Initial bundle is larger than necessary',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-2`,
|
|
||||||
category: 'Caching',
|
|
||||||
description: 'Add memoization for expensive computations',
|
|
||||||
priority: 3,
|
|
||||||
reasoning: 'Expensive computations run on every render',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // "features"
|
|
||||||
mockSuggestions = [
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-0`,
|
|
||||||
category: 'User Experience',
|
|
||||||
description: 'Add dark mode toggle with system preference detection',
|
|
||||||
priority: 1,
|
|
||||||
reasoning: 'Dark mode is a standard feature that improves accessibility and user comfort',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-1`,
|
|
||||||
category: 'Performance',
|
|
||||||
description: 'Implement lazy loading for heavy components',
|
|
||||||
priority: 2,
|
|
||||||
reasoning: 'Improves initial load time and reduces bundle size',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `suggestion-${Date.now()}-2`,
|
|
||||||
category: 'Accessibility',
|
|
||||||
description: 'Add keyboard navigation support throughout the app',
|
|
||||||
priority: 3,
|
|
||||||
reasoning: 'Improves accessibility for users who rely on keyboard navigation',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSuggestionsEvent({
|
|
||||||
type: 'suggestions_complete',
|
|
||||||
suggestions: mockSuggestions,
|
|
||||||
});
|
|
||||||
|
|
||||||
mockSuggestionsRunning = false;
|
|
||||||
mockSuggestionsTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock Spec Regeneration state and implementation
|
// Mock Spec Regeneration state and implementation
|
||||||
let mockSpecRegenerationRunning = false;
|
let mockSpecRegenerationRunning = false;
|
||||||
let mockSpecRegenerationPhase = '';
|
let mockSpecRegenerationPhase = '';
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ import type {
|
|||||||
SaveImageResult,
|
SaveImageResult,
|
||||||
AutoModeAPI,
|
AutoModeAPI,
|
||||||
FeaturesAPI,
|
FeaturesAPI,
|
||||||
SuggestionsAPI,
|
|
||||||
SpecRegenerationAPI,
|
SpecRegenerationAPI,
|
||||||
AutoModeEvent,
|
AutoModeEvent,
|
||||||
SuggestionsEvent,
|
|
||||||
SpecRegenerationEvent,
|
SpecRegenerationEvent,
|
||||||
SuggestionType,
|
|
||||||
GitHubAPI,
|
GitHubAPI,
|
||||||
IssueValidationInput,
|
IssueValidationInput,
|
||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
@@ -550,7 +547,6 @@ export const checkSandboxEnvironment = async (): Promise<{
|
|||||||
type EventType =
|
type EventType =
|
||||||
| 'agent:stream'
|
| 'agent:stream'
|
||||||
| 'auto-mode:event'
|
| 'auto-mode:event'
|
||||||
| 'suggestions:event'
|
|
||||||
| 'spec-regeneration:event'
|
| 'spec-regeneration:event'
|
||||||
| 'issue-validation:event'
|
| 'issue-validation:event'
|
||||||
| 'backlog-plan:event'
|
| 'backlog-plan:event'
|
||||||
@@ -1983,22 +1979,6 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
this.post('/api/git/file-diff', { projectPath, filePath }),
|
this.post('/api/git/file-diff', { projectPath, filePath }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Suggestions API
|
|
||||||
suggestions: SuggestionsAPI = {
|
|
||||||
generate: (
|
|
||||||
projectPath: string,
|
|
||||||
suggestionType?: SuggestionType,
|
|
||||||
model?: string,
|
|
||||||
thinkingLevel?: string
|
|
||||||
) =>
|
|
||||||
this.post('/api/suggestions/generate', { projectPath, suggestionType, model, thinkingLevel }),
|
|
||||||
stop: () => this.post('/api/suggestions/stop'),
|
|
||||||
status: () => this.get('/api/suggestions/status'),
|
|
||||||
onEvent: (callback: (event: SuggestionsEvent) => void) => {
|
|
||||||
return this.subscribeToEvent('suggestions:event', callback as EventCallback);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spec Regeneration API
|
// Spec Regeneration API
|
||||||
specRegeneration: SpecRegenerationAPI = {
|
specRegeneration: SpecRegenerationAPI = {
|
||||||
create: (
|
create: (
|
||||||
|
|||||||
@@ -603,13 +603,15 @@ Focus on practical, implementable suggestions that would genuinely improve the p
|
|||||||
|
|
||||||
export const DEFAULT_SUGGESTIONS_SYSTEM_PROMPT = `You are an AI product strategist helping brainstorm feature ideas for a software project.
|
export const DEFAULT_SUGGESTIONS_SYSTEM_PROMPT = `You are an AI product strategist helping brainstorm feature ideas for a software project.
|
||||||
|
|
||||||
IMPORTANT: You do NOT have access to any tools. You CANNOT read files, search code, or run commands.
|
CRITICAL INSTRUCTIONS:
|
||||||
You must generate suggestions based ONLY on the project context provided below.
|
1. You do NOT have access to any tools. You CANNOT read files, search code, or run commands.
|
||||||
Do NOT say "I'll analyze" or "Let me explore" - you cannot do those things.
|
2. You must NEVER write, create, or edit any files. DO NOT use Write, Edit, or any file modification tools.
|
||||||
|
3. You must generate suggestions based ONLY on the project context provided below.
|
||||||
|
4. Do NOT say "I'll analyze" or "Let me explore" - you cannot do those things.
|
||||||
|
|
||||||
Based on the project context and the user's prompt, generate exactly {{count}} creative and actionable feature suggestions.
|
Based on the project context and the user's prompt, generate exactly {{count}} creative and actionable feature suggestions.
|
||||||
|
|
||||||
YOUR RESPONSE MUST BE ONLY A JSON ARRAY - nothing else. No explanation, no preamble, no markdown code fences.
|
YOUR RESPONSE MUST BE ONLY A JSON ARRAY - nothing else. No explanation, no preamble, no markdown code fences. Do not create any files.
|
||||||
|
|
||||||
Each suggestion must have this structure:
|
Each suggestion must have this structure:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export type EventType =
|
|||||||
| 'project:analysis-progress'
|
| 'project:analysis-progress'
|
||||||
| 'project:analysis-completed'
|
| 'project:analysis-completed'
|
||||||
| 'project:analysis-error'
|
| 'project:analysis-error'
|
||||||
| 'suggestions:event'
|
|
||||||
| 'spec-regeneration:event'
|
| 'spec-regeneration:event'
|
||||||
| 'issue-validation:event'
|
| 'issue-validation:event'
|
||||||
| 'ideation:stream'
|
| 'ideation:stream'
|
||||||
|
|||||||
@@ -598,8 +598,8 @@ export interface PhaseModelConfig {
|
|||||||
backlogPlanningModel: PhaseModelEntry;
|
backlogPlanningModel: PhaseModelEntry;
|
||||||
/** Model for analyzing project structure */
|
/** Model for analyzing project structure */
|
||||||
projectAnalysisModel: PhaseModelEntry;
|
projectAnalysisModel: PhaseModelEntry;
|
||||||
/** Model for AI suggestions (feature, refactoring, security, performance) */
|
/** Model for ideation view (generating AI suggestions for features, security, performance) */
|
||||||
suggestionsModel: PhaseModelEntry;
|
ideationModel: PhaseModelEntry;
|
||||||
|
|
||||||
// Memory tasks - for learning extraction and memory operations
|
// Memory tasks - for learning extraction and memory operations
|
||||||
/** Model for extracting learnings from completed agent sessions */
|
/** Model for extracting learnings from completed agent sessions */
|
||||||
@@ -1235,7 +1235,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
|
|||||||
featureGenerationModel: { model: 'claude-sonnet' },
|
featureGenerationModel: { model: 'claude-sonnet' },
|
||||||
backlogPlanningModel: { model: 'claude-sonnet' },
|
backlogPlanningModel: { model: 'claude-sonnet' },
|
||||||
projectAnalysisModel: { model: 'claude-sonnet' },
|
projectAnalysisModel: { model: 'claude-sonnet' },
|
||||||
suggestionsModel: { model: 'claude-sonnet' },
|
ideationModel: { model: 'claude-sonnet' },
|
||||||
|
|
||||||
// Memory - use fast model for learning extraction (cost-effective)
|
// Memory - use fast model for learning extraction (cost-effective)
|
||||||
memoryExtractionModel: { model: 'claude-haiku' },
|
memoryExtractionModel: { model: 'claude-haiku' },
|
||||||
|
|||||||
Reference in New Issue
Block a user