diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts
index 9fbc5375a..653baeda0 100644
--- a/apps/server/src/index.ts
+++ b/apps/server/src/index.ts
@@ -43,7 +43,6 @@ import { createEnhancePromptRoutes } from './routes/enhance-prompt/index.js';
import { createWorktreeRoutes } from './routes/worktree/index.js';
import { createGitRoutes } from './routes/git/index.js';
import { createSetupRoutes } from './routes/setup/index.js';
-import { createSuggestionsRoutes } from './routes/suggestions/index.js';
import { createModelsRoutes } from './routes/models/index.js';
import { createRunningAgentsRoutes } from './routes/running-agents/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/worktree', createWorktreeRoutes(events, settingsService));
app.use('/api/git', createGitRoutes());
-app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService));
app.use('/api/models', createModelsRoutes());
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
diff --git a/apps/server/src/providers/cursor-provider.ts b/apps/server/src/providers/cursor-provider.ts
index 6cefc279f..8e62ce045 100644
--- a/apps/server/src/providers/cursor-provider.ts
+++ b/apps/server/src/providers/cursor-provider.ts
@@ -337,10 +337,11 @@ export class CursorProvider extends CliProvider {
'--stream-partial-output' // Real-time streaming
);
- // Only add --force if NOT in read-only mode
- // Without --force, Cursor CLI suggests changes but doesn't apply them
- // With --force, Cursor CLI can actually edit files
- if (!options.readOnly) {
+ // In read-only mode, use --mode ask for Q&A style (no tools)
+ // Otherwise, add --force to allow file edits
+ if (options.readOnly) {
+ cliArgs.push('--mode', 'ask');
+ } else {
cliArgs.push('--force');
}
@@ -672,10 +673,13 @@ export class CursorProvider extends CliProvider {
);
}
+ // Embed system prompt into user prompt (Cursor CLI doesn't support separate system messages)
+ const effectiveOptions = this.embedSystemPromptIntoPrompt(options);
+
// Extract prompt text to pass via stdin (avoids shell escaping issues)
- const promptText = this.extractPromptText(options);
+ const promptText = this.extractPromptText(effectiveOptions);
- const cliArgs = this.buildCliArgs(options);
+ const cliArgs = this.buildCliArgs(effectiveOptions);
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
// Pass prompt via stdin to avoid shell interpretation of special characters
diff --git a/apps/server/src/routes/suggestions/common.ts b/apps/server/src/routes/suggestions/common.ts
deleted file mode 100644
index e4e3dbe81..000000000
--- a/apps/server/src/routes/suggestions/common.ts
+++ /dev/null
@@ -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);
diff --git a/apps/server/src/routes/suggestions/generate-suggestions.ts b/apps/server/src/routes/suggestions/generate-suggestions.ts
deleted file mode 100644
index b828a4ab1..000000000
--- a/apps/server/src/routes/suggestions/generate-suggestions.ts
+++ /dev/null
@@ -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 ... section
- const implementedMatch = specContent.match(
- /([\s\S]*?)<\/implemented_features>/
- );
-
- if (implementedMatch) {
- const implementedSection = implementedMatch[1];
-
- // Extract feature names from ... tags using matchAll
- const nameRegex = /(.*?)<\/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 {
- 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 {
- // Get customized prompts from settings
- const prompts = await getPromptCustomization(settingsService, '[Suggestions]');
-
- // Map suggestion types to their prompts
- const typePrompts: Record = {
- 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> } | null = null;
-
- if (result.structured_output) {
- structuredOutput = result.structured_output as {
- suggestions: Array>;
- };
- 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> }>(
- responseText,
- 'suggestions',
- { logger }
- );
- }
-
- if (structuredOutput && structuredOutput.suggestions) {
- // Use structured output directly
- events.emit('suggestions:event', {
- type: 'suggestions_complete',
- suggestions: structuredOutput.suggestions.map((s: Record, 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',
- },
- ],
- });
- }
-}
diff --git a/apps/server/src/routes/suggestions/index.ts b/apps/server/src/routes/suggestions/index.ts
deleted file mode 100644
index 01e228790..000000000
--- a/apps/server/src/routes/suggestions/index.ts
+++ /dev/null
@@ -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;
-}
diff --git a/apps/server/src/routes/suggestions/routes/generate.ts b/apps/server/src/routes/suggestions/routes/generate.ts
deleted file mode 100644
index 6ce2427b4..000000000
--- a/apps/server/src/routes/suggestions/routes/generate.ts
+++ /dev/null
@@ -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 => {
- 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) });
- }
- };
-}
diff --git a/apps/server/src/routes/suggestions/routes/status.ts b/apps/server/src/routes/suggestions/routes/status.ts
deleted file mode 100644
index eb135e062..000000000
--- a/apps/server/src/routes/suggestions/routes/status.ts
+++ /dev/null
@@ -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 => {
- 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) });
- }
- };
-}
diff --git a/apps/server/src/routes/suggestions/routes/stop.ts b/apps/server/src/routes/suggestions/routes/stop.ts
deleted file mode 100644
index f9e01fb65..000000000
--- a/apps/server/src/routes/suggestions/routes/stop.ts
+++ /dev/null
@@ -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 => {
- 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) });
- }
- };
-}
diff --git a/apps/server/src/services/ideation-service.ts b/apps/server/src/services/ideation-service.ts
index 0a6a84714..990a45529 100644
--- a/apps/server/src/services/ideation-service.ts
+++ b/apps/server/src/services/ideation-service.ts
@@ -39,9 +39,13 @@ import { ProviderFactory } from '../providers/provider-factory.js';
import type { SettingsService } from './settings-service.js';
import type { FeatureLoader } from './feature-loader.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 { getPromptCustomization, getProviderByModelId } from '../lib/settings-helpers.js';
+import {
+ getPromptCustomization,
+ getProviderByModelId,
+ getPhaseModelWithOverrides,
+} from '../lib/settings-helpers.js';
const logger = createLogger('IdeationService');
@@ -684,8 +688,24 @@ export class IdeationService {
existingWorkContext
);
- // Resolve model alias to canonical identifier (with prefix)
- const modelId = resolveModelString('sonnet');
+ // Get model from phase settings with provider info (ideationModel)
+ 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
const sdkOptions = createChatOptions({
@@ -700,9 +720,6 @@ export class IdeationService {
// Strip provider prefix - providers need bare model IDs
const bareModel = stripProviderPrefix(modelId);
- // Get credentials for API calls (uses hardcoded model, no phase setting)
- const credentials = await this.settingsService?.getCredentials();
-
const executeOptions: ExecuteOptions = {
prompt: prompt.prompt,
model: bareModel,
@@ -713,6 +730,8 @@ export class IdeationService {
// Disable all tools - we just want text generation, not codebase analysis
allowedTools: [],
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
};
diff --git a/apps/ui/scripts/setup-e2e-fixtures.mjs b/apps/ui/scripts/setup-e2e-fixtures.mjs
index 356e419b2..6bfe55bed 100644
--- a/apps/ui/scripts/setup-e2e-fixtures.mjs
+++ b/apps/ui/scripts/setup-e2e-fixtures.mjs
@@ -58,7 +58,7 @@ const E2E_SETTINGS = {
featureGenerationModel: { model: 'sonnet' },
backlogPlanningModel: { model: 'sonnet' },
projectAnalysisModel: { model: 'sonnet' },
- suggestionsModel: { model: 'sonnet' },
+ ideationModel: { model: 'sonnet' },
},
enhancementModel: 'sonnet',
validationModel: 'opus',
diff --git a/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx b/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx
index a402b8d17..8833bb303 100644
--- a/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx
+++ b/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx
@@ -11,7 +11,6 @@ import { useIdeationStore } from '@/store/ideation-store';
import { useAppStore } from '@/store/app-store';
import { useGenerateIdeationSuggestions } from '@/hooks/mutations';
import { toast } from 'sonner';
-import { useNavigate } from '@tanstack/react-router';
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
interface PromptListProps {
@@ -24,10 +23,8 @@ export function PromptList({ category, onBack }: PromptListProps) {
const generationJobs = useIdeationStore((s) => s.generationJobs);
const setMode = useIdeationStore((s) => s.setMode);
const addGenerationJob = useIdeationStore((s) => s.addGenerationJob);
- const updateJobStatus = useIdeationStore((s) => s.updateJobStatus);
const [loadingPromptId, setLoadingPromptId] = useState(null);
const [startedPrompts, setStartedPrompts] = useState>(new Set());
- const navigate = useNavigate();
// React Query mutation
const generateMutation = useGenerateIdeationSuggestions(currentProject?.path ?? '');
@@ -72,27 +69,13 @@ export function PromptList({ category, onBack }: PromptListProps) {
toast.info(`Generating ideas for "${prompt.title}"...`);
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(
- { promptId: prompt.id, category },
+ { promptId: prompt.id, category, jobId, promptTitle: prompt.title },
{
- onSuccess: (data) => {
- updateJobStatus(jobId, 'ready', data.suggestions);
- 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);
+ // Optional: reset local loading state if component is still mounted
+ onSettled: () => {
setLoadingPromptId(null);
},
}
diff --git a/apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx b/apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx
index c6209d5e0..526702636 100644
--- a/apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx
+++ b/apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx
@@ -44,7 +44,7 @@ const PHASE_LABELS: Record = {
featureGenerationModel: 'Feature Generation',
backlogPlanningModel: 'Backlog Planning',
projectAnalysisModel: 'Project Analysis',
- suggestionsModel: 'AI Suggestions',
+ ideationModel: 'Ideation',
memoryExtractionModel: 'Memory Extraction',
};
diff --git a/apps/ui/src/components/views/project-settings-view/project-models-section.tsx b/apps/ui/src/components/views/project-settings-view/project-models-section.tsx
index e0e1f1bab..5102d243f 100644
--- a/apps/ui/src/components/views/project-settings-view/project-models-section.tsx
+++ b/apps/ui/src/components/views/project-settings-view/project-models-section.tsx
@@ -72,9 +72,9 @@ const GENERATION_TASKS: PhaseConfig[] = [
description: 'Analyzes project structure for suggestions',
},
{
- key: 'suggestionsModel',
- label: 'AI Suggestions',
- description: 'Model for feature, refactoring, security, and performance suggestions',
+ key: 'ideationModel',
+ label: 'Ideation',
+ description: 'Model for ideation view (generating AI suggestions)',
},
];
diff --git a/apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx b/apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx
index 29be327e3..21b3f153f 100644
--- a/apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx
+++ b/apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx
@@ -42,7 +42,7 @@ const PHASE_LABELS: Record = {
featureGenerationModel: 'Feature Generation',
backlogPlanningModel: 'Backlog Planning',
projectAnalysisModel: 'Project Analysis',
- suggestionsModel: 'AI Suggestions',
+ ideationModel: 'Ideation',
memoryExtractionModel: 'Memory Extraction',
};
diff --git a/apps/ui/src/components/views/settings-view/model-defaults/model-defaults-section.tsx b/apps/ui/src/components/views/settings-view/model-defaults/model-defaults-section.tsx
index 2fb4c9d38..9652f0741 100644
--- a/apps/ui/src/components/views/settings-view/model-defaults/model-defaults-section.tsx
+++ b/apps/ui/src/components/views/settings-view/model-defaults/model-defaults-section.tsx
@@ -67,9 +67,9 @@ const GENERATION_TASKS: PhaseConfig[] = [
description: 'Analyzes project structure for suggestions',
},
{
- key: 'suggestionsModel',
- label: 'AI Suggestions',
- description: 'Model for feature, refactoring, security, and performance suggestions',
+ key: 'ideationModel',
+ label: 'Ideation',
+ description: 'Model for ideation view (generating AI suggestions)',
},
];
diff --git a/apps/ui/src/hooks/mutations/use-ideation-mutations.ts b/apps/ui/src/hooks/mutations/use-ideation-mutations.ts
index 61841d9e7..2c81b3eeb 100644
--- a/apps/ui/src/hooks/mutations/use-ideation-mutations.ts
+++ b/apps/ui/src/hooks/mutations/use-ideation-mutations.ts
@@ -8,7 +8,8 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getElectronAPI } from '@/lib/electron';
import { queryKeys } from '@/lib/query-keys';
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
@@ -16,15 +17,23 @@ import type { IdeaCategory, IdeaSuggestion } from '@automaker/types';
interface GenerateSuggestionsInput {
promptId: string;
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
*/
interface GenerateSuggestionsResult {
- suggestions: IdeaSuggestion[];
+ suggestions: AnalysisSuggestion[];
promptId: string;
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({
mutationFn: async (input: GenerateSuggestionsInput): Promise => {
- const { promptId, category } = input;
+ const { promptId, category, jobId, promptTitle } = input;
const api = getElectronAPI();
if (!api.ideation?.generateSuggestions) {
@@ -69,14 +78,33 @@ export function useGenerateIdeationSuggestions(projectPath: string) {
suggestions: result.suggestions ?? [],
promptId,
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
queryClient.invalidateQueries({
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}`);
+ },
});
}
diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts
index d4679b815..c74923877 100644
--- a/apps/ui/src/hooks/use-settings-sync.ts
+++ b/apps/ui/src/hooks/use-settings-sync.ts
@@ -596,7 +596,7 @@ export async function refreshSettingsFromServer(): Promise {
projectAnalysisModel: migratePhaseModelEntry(
serverSettings.phaseModels.projectAnalysisModel
),
- suggestionsModel: migratePhaseModelEntry(serverSettings.phaseModels.suggestionsModel),
+ ideationModel: migratePhaseModelEntry(serverSettings.phaseModels.ideationModel),
memoryExtractionModel: migratePhaseModelEntry(
serverSettings.phaseModels.memoryExtractionModel
),
diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts
index f3f8939bc..b32f52f16 100644
--- a/apps/ui/src/lib/electron.ts
+++ b/apps/ui/src/lib/electron.ts
@@ -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
export type SpecRegenerationEvent =
| { type: 'spec_regeneration_progress'; content: string; projectPath: string }
@@ -702,7 +668,6 @@ export interface ElectronAPI {
};
worktree?: WorktreeAPI;
git?: GitAPI;
- suggestions?: SuggestionsAPI;
specRegeneration?: SpecRegenerationAPI;
autoMode?: AutoModeAPI;
features?: FeaturesAPI;
@@ -1333,9 +1298,6 @@ const getMockElectronAPI = (): ElectronAPI => {
// Mock Git API (for non-worktree operations)
git: createMockGitAPI(),
- // Mock Suggestions API
- suggestions: createMockSuggestionsAPI(),
-
// Mock Spec Regeneration API
specRegeneration: createMockSpecRegenerationAPI(),
@@ -2604,226 +2566,6 @@ function delay(ms: number, featureId: string): Promise {
});
}
-// 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 = {
- 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
let mockSpecRegenerationRunning = false;
let mockSpecRegenerationPhase = '';
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index 3d818da3b..9dd863cfe 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -16,12 +16,9 @@ import type {
SaveImageResult,
AutoModeAPI,
FeaturesAPI,
- SuggestionsAPI,
SpecRegenerationAPI,
AutoModeEvent,
- SuggestionsEvent,
SpecRegenerationEvent,
- SuggestionType,
GitHubAPI,
IssueValidationInput,
IssueValidationEvent,
@@ -550,7 +547,6 @@ export const checkSandboxEnvironment = async (): Promise<{
type EventType =
| 'agent:stream'
| 'auto-mode:event'
- | 'suggestions:event'
| 'spec-regeneration:event'
| 'issue-validation:event'
| 'backlog-plan:event'
@@ -1981,22 +1977,6 @@ export class HttpApiClient implements ElectronAPI {
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
specRegeneration: SpecRegenerationAPI = {
create: (
diff --git a/libs/prompts/src/defaults.ts b/libs/prompts/src/defaults.ts
index 550f635da..52413b3a7 100644
--- a/libs/prompts/src/defaults.ts
+++ b/libs/prompts/src/defaults.ts
@@ -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.
-IMPORTANT: You do NOT have access to any tools. You CANNOT read files, search code, or run commands.
-You must generate suggestions based ONLY on the project context provided below.
-Do NOT say "I'll analyze" or "Let me explore" - you cannot do those things.
+CRITICAL INSTRUCTIONS:
+1. You do NOT have access to any tools. You CANNOT read files, search code, or run commands.
+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.
-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:
{
diff --git a/libs/types/src/event.ts b/libs/types/src/event.ts
index 43f1d3d4e..281f88d8a 100644
--- a/libs/types/src/event.ts
+++ b/libs/types/src/event.ts
@@ -25,7 +25,6 @@ export type EventType =
| 'project:analysis-progress'
| 'project:analysis-completed'
| 'project:analysis-error'
- | 'suggestions:event'
| 'spec-regeneration:event'
| 'issue-validation:event'
| 'ideation:stream'
diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts
index cf2de7e47..c33036ebe 100644
--- a/libs/types/src/settings.ts
+++ b/libs/types/src/settings.ts
@@ -598,8 +598,8 @@ export interface PhaseModelConfig {
backlogPlanningModel: PhaseModelEntry;
/** Model for analyzing project structure */
projectAnalysisModel: PhaseModelEntry;
- /** Model for AI suggestions (feature, refactoring, security, performance) */
- suggestionsModel: PhaseModelEntry;
+ /** Model for ideation view (generating AI suggestions for features, security, performance) */
+ ideationModel: PhaseModelEntry;
// Memory tasks - for learning extraction and memory operations
/** Model for extracting learnings from completed agent sessions */
@@ -1235,7 +1235,7 @@ export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
featureGenerationModel: { model: 'claude-sonnet' },
backlogPlanningModel: { model: 'claude-sonnet' },
projectAnalysisModel: { model: 'claude-sonnet' },
- suggestionsModel: { model: 'claude-sonnet' },
+ ideationModel: { model: 'claude-sonnet' },
// Memory - use fast model for learning extraction (cost-effective)
memoryExtractionModel: { model: 'claude-haiku' },