diff --git a/.claude/.gitignore b/.claude/.gitignore new file mode 100644 index 00000000..735e81ff --- /dev/null +++ b/.claude/.gitignore @@ -0,0 +1 @@ +hans/ \ No newline at end of file diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 0f97255f..ab53a579 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -58,6 +58,8 @@ import { createMCPRoutes } from './routes/mcp/index.js'; import { MCPTestService } from './services/mcp-test-service.js'; import { createPipelineRoutes } from './routes/pipeline/index.js'; import { pipelineService } from './services/pipeline-service.js'; +import { createIdeationRoutes } from './routes/ideation/index.js'; +import { IdeationService } from './services/ideation-service.js'; // Load environment variables dotenv.config(); @@ -162,6 +164,7 @@ const featureLoader = new FeatureLoader(); const autoModeService = new AutoModeService(events, settingsService); const claudeUsageService = new ClaudeUsageService(); const mcpTestService = new MCPTestService(settingsService); +const ideationService = new IdeationService(events, settingsService, featureLoader); // Initialize services (async () => { @@ -215,6 +218,7 @@ app.use('/api/context', createContextRoutes(settingsService)); app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService)); app.use('/api/mcp', createMCPRoutes(mcpTestService)); app.use('/api/pipeline', createPipelineRoutes(pipelineService)); +app.use('/api/ideation', createIdeationRoutes(ideationService, featureLoader)); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index 33494535..fc380dfc 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -89,7 +89,7 @@ export class ClaudeProvider extends BaseProvider { ...(allowedTools && shouldRestrictTools && { allowedTools }), ...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }), // When MCP servers are configured and auto-approve is enabled, use bypassPermissions - permissionMode: shouldBypassPermissions ? 'bypassPermissions' : 'default', + permissionMode: shouldBypassPermissions ? 'bypassPermissions' : 'acceptEdits', // Required when using bypassPermissions mode ...(shouldBypassPermissions && { allowDangerouslySkipPermissions: true }), abortController, diff --git a/apps/server/src/routes/ideation/common.ts b/apps/server/src/routes/ideation/common.ts new file mode 100644 index 00000000..2cca3654 --- /dev/null +++ b/apps/server/src/routes/ideation/common.ts @@ -0,0 +1,12 @@ +/** + * Common utilities for ideation routes + */ + +import { createLogger } from '@automaker/utils'; +import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js'; + +const logger = createLogger('Ideation'); + +// Re-export shared utilities +export { getErrorMessageShared as getErrorMessage }; +export const logError = createLogError(logger); diff --git a/apps/server/src/routes/ideation/index.ts b/apps/server/src/routes/ideation/index.ts new file mode 100644 index 00000000..cd64c739 --- /dev/null +++ b/apps/server/src/routes/ideation/index.ts @@ -0,0 +1,99 @@ +/** + * Ideation routes - HTTP API for brainstorming and idea management + */ + +import { Router } from 'express'; +import { validatePathParams } from '../../middleware/validate-paths.js'; +import type { IdeationService } from '../../services/ideation-service.js'; +import type { FeatureLoader } from '../../services/feature-loader.js'; + +// Route handlers +import { createSessionStartHandler } from './routes/session-start.js'; +import { createSessionMessageHandler } from './routes/session-message.js'; +import { createSessionStopHandler } from './routes/session-stop.js'; +import { createSessionGetHandler } from './routes/session-get.js'; +import { createIdeasListHandler } from './routes/ideas-list.js'; +import { createIdeasCreateHandler } from './routes/ideas-create.js'; +import { createIdeasGetHandler } from './routes/ideas-get.js'; +import { createIdeasUpdateHandler } from './routes/ideas-update.js'; +import { createIdeasDeleteHandler } from './routes/ideas-delete.js'; +import { createAnalyzeHandler, createGetAnalysisHandler } from './routes/analyze.js'; +import { createConvertHandler } from './routes/convert.js'; +import { createPromptsHandler, createPromptsByCategoryHandler } from './routes/prompts.js'; +import { createSuggestionsGenerateHandler } from './routes/suggestions-generate.js'; + +export function createIdeationRoutes( + ideationService: IdeationService, + featureLoader: FeatureLoader +): Router { + const router = Router(); + + // Session management + router.post( + '/session/start', + validatePathParams('projectPath'), + createSessionStartHandler(ideationService) + ); + router.post('/session/message', createSessionMessageHandler(ideationService)); + router.post('/session/stop', createSessionStopHandler(ideationService)); + router.post( + '/session/get', + validatePathParams('projectPath'), + createSessionGetHandler(ideationService) + ); + + // Ideas CRUD + router.post( + '/ideas/list', + validatePathParams('projectPath'), + createIdeasListHandler(ideationService) + ); + router.post( + '/ideas/create', + validatePathParams('projectPath'), + createIdeasCreateHandler(ideationService) + ); + router.post( + '/ideas/get', + validatePathParams('projectPath'), + createIdeasGetHandler(ideationService) + ); + router.post( + '/ideas/update', + validatePathParams('projectPath'), + createIdeasUpdateHandler(ideationService) + ); + router.post( + '/ideas/delete', + validatePathParams('projectPath'), + createIdeasDeleteHandler(ideationService) + ); + + // Project analysis + router.post('/analyze', validatePathParams('projectPath'), createAnalyzeHandler(ideationService)); + router.post( + '/analysis', + validatePathParams('projectPath'), + createGetAnalysisHandler(ideationService) + ); + + // Convert to feature + router.post( + '/convert', + validatePathParams('projectPath'), + createConvertHandler(ideationService, featureLoader) + ); + + // Guided prompts (no validation needed - static data) + router.get('/prompts', createPromptsHandler(ideationService)); + router.get('/prompts/:category', createPromptsByCategoryHandler(ideationService)); + + // Generate suggestions (structured output) + router.post( + '/suggestions/generate', + validatePathParams('projectPath'), + createSuggestionsGenerateHandler(ideationService) + ); + + return router; +} diff --git a/apps/server/src/routes/ideation/routes/analyze.ts b/apps/server/src/routes/ideation/routes/analyze.ts new file mode 100644 index 00000000..e8e0b213 --- /dev/null +++ b/apps/server/src/routes/ideation/routes/analyze.ts @@ -0,0 +1,49 @@ +/** + * POST /analyze - Analyze project and generate suggestions + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createAnalyzeHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body as { projectPath: string }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + // Start analysis - results come via WebSocket events + ideationService.analyzeProject(projectPath).catch((error) => { + logError(error, 'Analyze project failed (async)'); + }); + + res.json({ success: true, message: 'Analysis started' }); + } catch (error) { + logError(error, 'Analyze project failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} + +export function createGetAnalysisHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body as { projectPath: string }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + const result = await ideationService.getCachedAnalysis(projectPath); + res.json({ success: true, result }); + } catch (error) { + logError(error, 'Get analysis failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/convert.ts b/apps/server/src/routes/ideation/routes/convert.ts new file mode 100644 index 00000000..ab83164d --- /dev/null +++ b/apps/server/src/routes/ideation/routes/convert.ts @@ -0,0 +1,61 @@ +/** + * POST /convert - Convert an idea to a feature + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { FeatureLoader } from '../../../services/feature-loader.js'; +import type { ConvertToFeatureOptions } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createConvertHandler( + ideationService: IdeationService, + featureLoader: FeatureLoader +) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, ideaId, keepIdea, column, dependencies, tags } = req.body as { + projectPath: string; + ideaId: string; + } & ConvertToFeatureOptions; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!ideaId) { + res.status(400).json({ success: false, error: 'ideaId is required' }); + return; + } + + // Convert idea to feature structure + const featureData = await ideationService.convertToFeature(projectPath, ideaId); + + // Apply any options from the request + if (column) { + featureData.status = column; + } + if (dependencies && dependencies.length > 0) { + featureData.dependencies = dependencies; + } + if (tags && tags.length > 0) { + featureData.tags = tags; + } + + // Create the feature using FeatureLoader + const feature = await featureLoader.create(projectPath, featureData); + + // Delete the idea unless keepIdea is explicitly true + if (!keepIdea) { + await ideationService.deleteIdea(projectPath, ideaId); + } + + // Return featureId as expected by the frontend API interface + res.json({ success: true, featureId: feature.id }); + } catch (error) { + logError(error, 'Convert to feature failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/ideas-create.ts b/apps/server/src/routes/ideation/routes/ideas-create.ts new file mode 100644 index 00000000..d854622e --- /dev/null +++ b/apps/server/src/routes/ideation/routes/ideas-create.ts @@ -0,0 +1,43 @@ +/** + * POST /ideas/create - Create a new idea + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { CreateIdeaInput } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createIdeasCreateHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, idea } = req.body as { + projectPath: string; + idea: CreateIdeaInput; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!idea) { + res.status(400).json({ success: false, error: 'idea is required' }); + return; + } + + if (!idea.title || !idea.description || !idea.category) { + res.status(400).json({ + success: false, + error: 'idea must have title, description, and category', + }); + return; + } + + const created = await ideationService.createIdea(projectPath, idea); + res.json({ success: true, idea: created }); + } catch (error) { + logError(error, 'Create idea failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/ideas-delete.ts b/apps/server/src/routes/ideation/routes/ideas-delete.ts new file mode 100644 index 00000000..931ae32a --- /dev/null +++ b/apps/server/src/routes/ideation/routes/ideas-delete.ts @@ -0,0 +1,34 @@ +/** + * POST /ideas/delete - Delete an idea + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createIdeasDeleteHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, ideaId } = req.body as { + projectPath: string; + ideaId: string; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!ideaId) { + res.status(400).json({ success: false, error: 'ideaId is required' }); + return; + } + + await ideationService.deleteIdea(projectPath, ideaId); + res.json({ success: true }); + } catch (error) { + logError(error, 'Delete idea failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/ideas-get.ts b/apps/server/src/routes/ideation/routes/ideas-get.ts new file mode 100644 index 00000000..d4865b46 --- /dev/null +++ b/apps/server/src/routes/ideation/routes/ideas-get.ts @@ -0,0 +1,39 @@ +/** + * POST /ideas/get - Get a single idea + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createIdeasGetHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, ideaId } = req.body as { + projectPath: string; + ideaId: string; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!ideaId) { + res.status(400).json({ success: false, error: 'ideaId is required' }); + return; + } + + const idea = await ideationService.getIdea(projectPath, ideaId); + if (!idea) { + res.status(404).json({ success: false, error: 'Idea not found' }); + return; + } + + res.json({ success: true, idea }); + } catch (error) { + logError(error, 'Get idea failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/ideas-list.ts b/apps/server/src/routes/ideation/routes/ideas-list.ts new file mode 100644 index 00000000..5f6b4504 --- /dev/null +++ b/apps/server/src/routes/ideation/routes/ideas-list.ts @@ -0,0 +1,26 @@ +/** + * POST /ideas/list - List all ideas for a project + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createIdeasListHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath } = req.body as { projectPath: string }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + const ideas = await ideationService.getIdeas(projectPath); + res.json({ success: true, ideas }); + } catch (error) { + logError(error, 'List ideas failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/ideas-update.ts b/apps/server/src/routes/ideation/routes/ideas-update.ts new file mode 100644 index 00000000..c2434ce4 --- /dev/null +++ b/apps/server/src/routes/ideation/routes/ideas-update.ts @@ -0,0 +1,46 @@ +/** + * POST /ideas/update - Update an idea + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { UpdateIdeaInput } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createIdeasUpdateHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, ideaId, updates } = req.body as { + projectPath: string; + ideaId: string; + updates: UpdateIdeaInput; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!ideaId) { + res.status(400).json({ success: false, error: 'ideaId is required' }); + return; + } + + if (!updates) { + res.status(400).json({ success: false, error: 'updates is required' }); + return; + } + + const idea = await ideationService.updateIdea(projectPath, ideaId, updates); + if (!idea) { + res.status(404).json({ success: false, error: 'Idea not found' }); + return; + } + + res.json({ success: true, idea }); + } catch (error) { + logError(error, 'Update idea failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/prompts.ts b/apps/server/src/routes/ideation/routes/prompts.ts new file mode 100644 index 00000000..fb54e1dd --- /dev/null +++ b/apps/server/src/routes/ideation/routes/prompts.ts @@ -0,0 +1,42 @@ +/** + * GET /prompts - Get all guided prompts + * GET /prompts/:category - Get prompts for a specific category + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { IdeaCategory } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createPromptsHandler(ideationService: IdeationService) { + return async (_req: Request, res: Response): Promise => { + try { + const prompts = ideationService.getAllPrompts(); + const categories = ideationService.getPromptCategories(); + res.json({ success: true, prompts, categories }); + } catch (error) { + logError(error, 'Get prompts failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} + +export function createPromptsByCategoryHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { category } = req.params as { category: string }; + + const validCategories: IdeaCategory[] = ['feature', 'ux-ui', 'dx', 'growth', 'technical']; + if (!validCategories.includes(category as IdeaCategory)) { + res.status(400).json({ success: false, error: 'Invalid category' }); + return; + } + + const prompts = ideationService.getPromptsByCategory(category as IdeaCategory); + res.json({ success: true, prompts }); + } catch (error) { + logError(error, 'Get prompts by category failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/session-get.ts b/apps/server/src/routes/ideation/routes/session-get.ts new file mode 100644 index 00000000..c95bd6cb --- /dev/null +++ b/apps/server/src/routes/ideation/routes/session-get.ts @@ -0,0 +1,45 @@ +/** + * POST /session/get - Get an ideation session with messages + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createSessionGetHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, sessionId } = req.body as { + projectPath: string; + sessionId: string; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!sessionId) { + res.status(400).json({ success: false, error: 'sessionId is required' }); + return; + } + + const session = await ideationService.getSession(projectPath, sessionId); + if (!session) { + res.status(404).json({ success: false, error: 'Session not found' }); + return; + } + + const isRunning = ideationService.isSessionRunning(sessionId); + + res.json({ + success: true, + session: { ...session, isRunning }, + messages: session.messages, + }); + } catch (error) { + logError(error, 'Get session failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/session-message.ts b/apps/server/src/routes/ideation/routes/session-message.ts new file mode 100644 index 00000000..0668583e --- /dev/null +++ b/apps/server/src/routes/ideation/routes/session-message.ts @@ -0,0 +1,40 @@ +/** + * POST /session/message - Send a message in an ideation session + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { SendMessageOptions } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createSessionMessageHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { sessionId, message, options } = req.body as { + sessionId: string; + message: string; + options?: SendMessageOptions; + }; + + if (!sessionId) { + res.status(400).json({ success: false, error: 'sessionId is required' }); + return; + } + + if (!message) { + res.status(400).json({ success: false, error: 'message is required' }); + return; + } + + // This is async but we don't await - responses come via WebSocket + ideationService.sendMessage(sessionId, message, options).catch((error) => { + logError(error, 'Send message failed (async)'); + }); + + res.json({ success: true }); + } catch (error) { + logError(error, 'Send message failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/session-start.ts b/apps/server/src/routes/ideation/routes/session-start.ts new file mode 100644 index 00000000..5d1ae838 --- /dev/null +++ b/apps/server/src/routes/ideation/routes/session-start.ts @@ -0,0 +1,30 @@ +/** + * POST /session/start - Start a new ideation session + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import type { StartSessionOptions } from '@automaker/types'; +import { getErrorMessage, logError } from '../common.js'; + +export function createSessionStartHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, options } = req.body as { + projectPath: string; + options?: StartSessionOptions; + }; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + const session = await ideationService.startSession(projectPath, options); + res.json({ success: true, session }); + } catch (error) { + logError(error, 'Start session failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/session-stop.ts b/apps/server/src/routes/ideation/routes/session-stop.ts new file mode 100644 index 00000000..858d7b7b --- /dev/null +++ b/apps/server/src/routes/ideation/routes/session-stop.ts @@ -0,0 +1,26 @@ +/** + * POST /session/stop - Stop an ideation session + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { getErrorMessage, logError } from '../common.js'; + +export function createSessionStopHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { sessionId } = req.body as { sessionId: string }; + + if (!sessionId) { + res.status(400).json({ success: false, error: 'sessionId is required' }); + return; + } + + await ideationService.stopSession(sessionId); + res.json({ success: true }); + } catch (error) { + logError(error, 'Stop session failed'); + res.status(500).json({ success: false, error: getErrorMessage(error) }); + } + }; +} diff --git a/apps/server/src/routes/ideation/routes/suggestions-generate.ts b/apps/server/src/routes/ideation/routes/suggestions-generate.ts new file mode 100644 index 00000000..6907b1af --- /dev/null +++ b/apps/server/src/routes/ideation/routes/suggestions-generate.ts @@ -0,0 +1,55 @@ +/** + * Generate suggestions route - Returns structured AI suggestions for a prompt + */ + +import type { Request, Response } from 'express'; +import type { IdeationService } from '../../../services/ideation-service.js'; +import { createLogger } from '@automaker/utils'; + +const logger = createLogger('ideation:suggestions-generate'); + +export function createSuggestionsGenerateHandler(ideationService: IdeationService) { + return async (req: Request, res: Response): Promise => { + try { + const { projectPath, promptId, category, count } = req.body; + + if (!projectPath) { + res.status(400).json({ success: false, error: 'projectPath is required' }); + return; + } + + if (!promptId) { + res.status(400).json({ success: false, error: 'promptId is required' }); + return; + } + + if (!category) { + res.status(400).json({ success: false, error: 'category is required' }); + return; + } + + // Default to 10 suggestions, allow 1-20 + const suggestionCount = Math.min(Math.max(count || 10, 1), 20); + + logger.info(`Generating ${suggestionCount} suggestions for prompt: ${promptId}`); + + const suggestions = await ideationService.generateSuggestions( + projectPath, + promptId, + category, + suggestionCount + ); + + res.json({ + success: true, + suggestions, + }); + } catch (error) { + logger.error('Failed to generate suggestions:', error); + res.status(500).json({ + success: false, + error: (error as Error).message, + }); + } + }; +} diff --git a/apps/server/src/services/ideation-service.ts b/apps/server/src/services/ideation-service.ts new file mode 100644 index 00000000..d2fde6dd --- /dev/null +++ b/apps/server/src/services/ideation-service.ts @@ -0,0 +1,1679 @@ +/** + * Ideation Service - Manages brainstorming sessions and ideas + * Provides AI-powered ideation, project analysis, and idea-to-feature conversion + */ + +import path from 'path'; +import * as secureFs from '../lib/secure-fs.js'; +import type { EventEmitter } from '../lib/events.js'; +import type { Feature, ExecuteOptions } from '@automaker/types'; +import type { + Idea, + IdeaCategory, + IdeaStatus, + IdeationSession, + IdeationSessionWithMessages, + IdeationMessage, + ProjectAnalysisResult, + AnalysisSuggestion, + AnalysisFileInfo, + CreateIdeaInput, + UpdateIdeaInput, + StartSessionOptions, + SendMessageOptions, + PromptCategory, + IdeationPrompt, +} from '@automaker/types'; +import { + getIdeationDir, + getIdeasDir, + getIdeaDir, + getIdeaPath, + getIdeationSessionsDir, + getIdeationSessionPath, + getIdeationAnalysisPath, + ensureIdeationDir, +} from '@automaker/platform'; +import { createLogger, loadContextFiles, isAbortError } from '@automaker/utils'; +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'; + +const logger = createLogger('IdeationService'); + +interface ActiveSession { + session: IdeationSession; + messages: IdeationMessage[]; + isRunning: boolean; + abortController: AbortController | null; +} + +export class IdeationService { + private activeSessions = new Map(); + private events: EventEmitter; + private settingsService: SettingsService | null = null; + private featureLoader: FeatureLoader | null = null; + + constructor( + events: EventEmitter, + settingsService?: SettingsService, + featureLoader?: FeatureLoader + ) { + this.events = events; + this.settingsService = settingsService ?? null; + this.featureLoader = featureLoader ?? null; + } + + // ============================================================================ + // Session Management + // ============================================================================ + + /** + * Start a new ideation session + */ + async startSession(projectPath: string, options?: StartSessionOptions): Promise { + validateWorkingDirectory(projectPath); + await ensureIdeationDir(projectPath); + + const sessionId = this.generateId('session'); + const now = new Date().toISOString(); + + const session: IdeationSession = { + id: sessionId, + projectPath, + promptCategory: options?.promptCategory, + promptId: options?.promptId, + status: 'active', + createdAt: now, + updatedAt: now, + }; + + const activeSession: ActiveSession = { + session, + messages: [], + isRunning: false, + abortController: null, + }; + + this.activeSessions.set(sessionId, activeSession); + await this.saveSessionToDisk(projectPath, session, []); + + this.events.emit('ideation:session-started', { sessionId, projectPath }); + + // If there's an initial message from a prompt, send it + if (options?.initialMessage) { + await this.sendMessage(sessionId, options.initialMessage); + } + + return session; + } + + /** + * Get an existing session + */ + async getSession( + projectPath: string, + sessionId: string + ): Promise { + // Check if session is already active in memory + let activeSession = this.activeSessions.get(sessionId); + + if (!activeSession) { + // Try to load from disk + const loaded = await this.loadSessionFromDisk(projectPath, sessionId); + if (!loaded) return null; + + activeSession = { + session: loaded.session, + messages: loaded.messages, + isRunning: false, + abortController: null, + }; + this.activeSessions.set(sessionId, activeSession); + } + + return { + ...activeSession.session, + messages: activeSession.messages, + }; + } + + /** + * Send a message in an ideation session + */ + async sendMessage( + sessionId: string, + message: string, + options?: SendMessageOptions + ): Promise { + const activeSession = this.activeSessions.get(sessionId); + if (!activeSession) { + throw new Error(`Session ${sessionId} not found`); + } + + if (activeSession.isRunning) { + throw new Error('Session is already processing a message'); + } + + activeSession.isRunning = true; + activeSession.abortController = new AbortController(); + + // Add user message + const userMessage: IdeationMessage = { + id: this.generateId('msg'), + role: 'user', + content: message, + timestamp: new Date().toISOString(), + }; + activeSession.messages.push(userMessage); + + // Emit user message + this.events.emit('ideation:stream', { + sessionId, + type: 'message', + message: userMessage, + }); + + try { + const projectPath = activeSession.session.projectPath; + + // Build conversation history + const conversationHistory = activeSession.messages.slice(0, -1).map((msg) => ({ + role: msg.role, + content: msg.content, + })); + + // Load context files + const contextResult = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // Gather existing features and ideas to prevent duplicate suggestions + const existingWorkContext = await this.gatherExistingWorkContext(projectPath); + + // Build system prompt for ideation + const systemPrompt = this.buildIdeationSystemPrompt( + contextResult.formattedPrompt, + activeSession.session.promptCategory, + existingWorkContext + ); + + // Create SDK options + const sdkOptions = createChatOptions({ + cwd: projectPath, + model: options?.model || 'sonnet', + systemPrompt, + abortController: activeSession.abortController!, + }); + + const effectiveModel = sdkOptions.model!; + const provider = ProviderFactory.getProviderForModel(effectiveModel); + + const executeOptions: ExecuteOptions = { + prompt: message, + model: effectiveModel, + cwd: projectPath, + systemPrompt: sdkOptions.systemPrompt, + maxTurns: 1, // Single turn for ideation + abortController: activeSession.abortController!, + conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined, + }; + + const stream = provider.executeQuery(executeOptions); + + let responseText = ''; + const assistantMessage: IdeationMessage = { + id: this.generateId('msg'), + role: 'assistant', + content: '', + timestamp: new Date().toISOString(), + }; + + for await (const msg of stream) { + if (msg.type === 'assistant' && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === 'text') { + responseText += block.text; + assistantMessage.content = responseText; + + this.events.emit('ideation:stream', { + sessionId, + type: 'stream', + content: responseText, + done: false, + }); + } + } + } else if (msg.type === 'result') { + if (msg.subtype === 'success' && msg.result) { + assistantMessage.content = msg.result; + responseText = msg.result; + } + } + } + + activeSession.messages.push(assistantMessage); + + this.events.emit('ideation:stream', { + sessionId, + type: 'message-complete', + message: assistantMessage, + content: responseText, + done: true, + }); + + // Save session + await this.saveSessionToDisk(projectPath, activeSession.session, activeSession.messages); + } catch (error) { + if (isAbortError(error)) { + this.events.emit('ideation:stream', { + sessionId, + type: 'aborted', + }); + } else { + logger.error('Error in ideation message:', error); + this.events.emit('ideation:stream', { + sessionId, + type: 'error', + error: (error as Error).message, + }); + } + } finally { + activeSession.isRunning = false; + activeSession.abortController = null; + } + } + + /** + * Stop an active session + */ + async stopSession(sessionId: string): Promise { + const activeSession = this.activeSessions.get(sessionId); + if (!activeSession) return; + + if (activeSession.abortController) { + activeSession.abortController.abort(); + } + + activeSession.isRunning = false; + activeSession.abortController = null; + activeSession.session.status = 'completed'; + + await this.saveSessionToDisk( + activeSession.session.projectPath, + activeSession.session, + activeSession.messages + ); + + this.events.emit('ideation:session-ended', { sessionId }); + } + + // ============================================================================ + // Ideas CRUD + // ============================================================================ + + /** + * Create a new idea + */ + async createIdea(projectPath: string, input: CreateIdeaInput): Promise { + validateWorkingDirectory(projectPath); + await ensureIdeationDir(projectPath); + + const ideaId = this.generateId('idea'); + const now = new Date().toISOString(); + + const idea: Idea = { + id: ideaId, + title: input.title, + description: input.description, + category: input.category, + status: input.status || 'raw', + impact: input.impact || 'medium', + effort: input.effort || 'medium', + conversationId: input.conversationId, + sourcePromptId: input.sourcePromptId, + userStories: input.userStories, + notes: input.notes, + createdAt: now, + updatedAt: now, + }; + + // Save to disk + const ideaDir = getIdeaDir(projectPath, ideaId); + await secureFs.mkdir(ideaDir, { recursive: true }); + await secureFs.writeFile( + getIdeaPath(projectPath, ideaId), + JSON.stringify(idea, null, 2), + 'utf-8' + ); + + return idea; + } + + /** + * Get all ideas for a project + */ + async getIdeas(projectPath: string): Promise { + try { + const ideasDir = getIdeasDir(projectPath); + + try { + await secureFs.access(ideasDir); + } catch { + return []; + } + + const entries = (await secureFs.readdir(ideasDir, { withFileTypes: true })) as any[]; + const ideaDirs = entries.filter((entry) => entry.isDirectory()); + + const ideas: Idea[] = []; + for (const dir of ideaDirs) { + try { + const ideaPath = getIdeaPath(projectPath, dir.name); + const content = (await secureFs.readFile(ideaPath, 'utf-8')) as string; + ideas.push(JSON.parse(content)); + } catch (error) { + logger.warn(`Failed to load idea ${dir.name}:`, error); + } + } + + // Sort by updatedAt descending + return ideas.sort( + (a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + } catch (error) { + logger.error('Failed to get ideas:', error); + return []; + } + } + + /** + * Get a single idea + */ + async getIdea(projectPath: string, ideaId: string): Promise { + try { + const ideaPath = getIdeaPath(projectPath, ideaId); + const content = (await secureFs.readFile(ideaPath, 'utf-8')) as string; + return JSON.parse(content); + } catch { + return null; + } + } + + /** + * Update an idea + */ + async updateIdea( + projectPath: string, + ideaId: string, + updates: UpdateIdeaInput + ): Promise { + const idea = await this.getIdea(projectPath, ideaId); + if (!idea) return null; + + const updatedIdea: Idea = { + ...idea, + ...updates, + updatedAt: new Date().toISOString(), + }; + + await secureFs.writeFile( + getIdeaPath(projectPath, ideaId), + JSON.stringify(updatedIdea, null, 2), + 'utf-8' + ); + + return updatedIdea; + } + + /** + * Delete an idea + */ + async deleteIdea(projectPath: string, ideaId: string): Promise { + const ideaDir = getIdeaDir(projectPath, ideaId); + try { + await secureFs.rm(ideaDir, { recursive: true }); + } catch { + // Ignore if doesn't exist + } + } + + /** + * Archive an idea + */ + async archiveIdea(projectPath: string, ideaId: string): Promise { + return this.updateIdea(projectPath, ideaId, { + status: 'archived' as IdeaStatus, + }); + } + + // ============================================================================ + // Project Analysis + // ============================================================================ + + /** + * Analyze project structure and generate suggestions + */ + async analyzeProject(projectPath: string): Promise { + validateWorkingDirectory(projectPath); + await ensureIdeationDir(projectPath); + + this.emitAnalysisEvent('ideation:analysis-started', { + projectPath, + message: 'Starting project analysis...', + }); + + try { + // Gather project structure + const structure = await this.gatherProjectStructure(projectPath); + + this.emitAnalysisEvent('ideation:analysis-progress', { + projectPath, + progress: 30, + message: 'Analyzing codebase structure...', + }); + + // Use AI to generate suggestions + const suggestions = await this.generateAnalysisSuggestions(projectPath, structure); + + this.emitAnalysisEvent('ideation:analysis-progress', { + projectPath, + progress: 80, + message: 'Generating improvement suggestions...', + }); + + const result: ProjectAnalysisResult = { + projectPath, + analyzedAt: new Date().toISOString(), + totalFiles: structure.totalFiles, + routes: structure.routes, + components: structure.components, + services: structure.services, + framework: structure.framework, + language: structure.language, + dependencies: structure.dependencies, + suggestions, + summary: this.generateAnalysisSummary(structure, suggestions), + }; + + // Cache the result + await secureFs.writeFile( + getIdeationAnalysisPath(projectPath), + JSON.stringify(result, null, 2), + 'utf-8' + ); + + this.emitAnalysisEvent('ideation:analysis-complete', { + projectPath, + result, + }); + + return result; + } catch (error) { + logger.error('Project analysis failed:', error); + this.emitAnalysisEvent('ideation:analysis-error', { + projectPath, + error: (error as Error).message, + }); + throw error; + } + } + + /** + * Emit analysis event wrapped in ideation:analysis format + */ + private emitAnalysisEvent(eventType: string, data: Record): void { + this.events.emit('ideation:analysis', { + type: eventType, + ...data, + }); + } + + /** + * Check if a session is currently running (processing a message) + */ + isSessionRunning(sessionId: string): boolean { + const activeSession = this.activeSessions.get(sessionId); + return activeSession?.isRunning ?? false; + } + + /** + * Get cached analysis result + */ + async getCachedAnalysis(projectPath: string): Promise { + try { + const content = (await secureFs.readFile( + getIdeationAnalysisPath(projectPath), + 'utf-8' + )) as string; + return JSON.parse(content); + } catch { + return null; + } + } + + // ============================================================================ + // Convert to Feature + // ============================================================================ + + /** + * Convert an idea to a feature + */ + async convertToFeature(projectPath: string, ideaId: string): Promise { + const idea = await this.getIdea(projectPath, ideaId); + if (!idea) { + throw new Error(`Idea ${ideaId} not found`); + } + + // Build feature description from idea + let description = idea.description; + if (idea.userStories && idea.userStories.length > 0) { + description += '\n\n## User Stories\n' + idea.userStories.map((s) => `- ${s}`).join('\n'); + } + if (idea.notes) { + description += '\n\n## Notes\n' + idea.notes; + } + + const feature: Feature = { + id: this.generateId('feature'), + title: idea.title, + category: this.mapIdeaCategoryToFeatureCategory(idea.category), + description, + status: 'backlog', + }; + + return feature; + } + + // ============================================================================ + // Generate Suggestions + // ============================================================================ + + /** + * Generate structured suggestions for a prompt + * Returns parsed suggestions that can be directly added to the board + */ + async generateSuggestions( + projectPath: string, + promptId: string, + category: IdeaCategory, + count: number = 10 + ): Promise { + validateWorkingDirectory(projectPath); + + // Get the prompt + const prompt = this.getAllPrompts().find((p) => p.id === promptId); + if (!prompt) { + throw new Error(`Prompt ${promptId} not found`); + } + + // Emit start event + this.events.emit('ideation:suggestions', { + type: 'started', + promptId, + category, + }); + + try { + // Load context files + const contextResult = await loadContextFiles({ + projectPath, + fsModule: secureFs as Parameters[0]['fsModule'], + }); + + // Build context from multiple sources + let contextPrompt = contextResult.formattedPrompt; + + // If no context files, try to gather basic project info + if (!contextPrompt) { + const projectInfo = await this.gatherBasicProjectInfo(projectPath); + if (projectInfo) { + contextPrompt = projectInfo; + } + } + + // Gather existing features and ideas to prevent duplicates + const existingWorkContext = await this.gatherExistingWorkContext(projectPath); + + // Build system prompt for structured suggestions + const systemPrompt = this.buildSuggestionsSystemPrompt( + contextPrompt, + category, + count, + existingWorkContext + ); + + // Create SDK options + const sdkOptions = createChatOptions({ + cwd: projectPath, + model: 'sonnet', + systemPrompt, + abortController: new AbortController(), + }); + + const effectiveModel = sdkOptions.model!; + const provider = ProviderFactory.getProviderForModel(effectiveModel); + + const executeOptions: ExecuteOptions = { + prompt: prompt.prompt, + model: effectiveModel, + cwd: projectPath, + systemPrompt: sdkOptions.systemPrompt, + maxTurns: 1, + // Disable all tools - we just want text generation, not codebase analysis + allowedTools: [], + abortController: new AbortController(), + }; + + const stream = provider.executeQuery(executeOptions); + + let responseText = ''; + for await (const msg of stream) { + if (msg.type === 'assistant' && msg.message?.content) { + for (const block of msg.message.content) { + if (block.type === 'text') { + responseText += block.text; + } + } + } else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) { + responseText = msg.result; + } + } + + // Parse the response into structured suggestions + const suggestions = this.parseSuggestionsFromResponse(responseText, category); + + // Emit complete event + this.events.emit('ideation:suggestions', { + type: 'complete', + promptId, + category, + suggestions, + }); + + return suggestions; + } catch (error) { + logger.error('Failed to generate suggestions:', error); + this.events.emit('ideation:suggestions', { + type: 'error', + promptId, + error: (error as Error).message, + }); + throw error; + } + } + + /** + * Build system prompt for structured suggestion generation + */ + private buildSuggestionsSystemPrompt( + contextFilesPrompt: string | undefined, + category: IdeaCategory, + count: number = 10, + existingWorkContext?: string + ): string { + const contextSection = contextFilesPrompt + ? `## Project Context\n${contextFilesPrompt}` + : `## No Project Context Available\nNo context files were found. Generate suggestions based on the user's prompt and general best practices for the type of application being described.`; + + const existingWorkSection = existingWorkContext ? `\n\n${existingWorkContext}` : ''; + + return `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. + +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. + +Each suggestion must have this structure: +{ + "title": "Short, actionable title (max 60 chars)", + "description": "Clear description of what to build or improve (2-3 sentences)", + "rationale": "Why this is valuable - the problem it solves or opportunity it creates", + "priority": "high" | "medium" | "low" +} + +Focus area: ${this.getCategoryDescription(category)} + +Guidelines: +- Generate exactly ${count} suggestions +- Be specific and actionable - avoid vague ideas +- Mix different priority levels (some high, some medium, some low) +- Each suggestion should be independently implementable +- Think creatively - include both obvious improvements and innovative ideas +- Consider the project's domain and target users +- IMPORTANT: Do NOT suggest features or ideas that already exist in the "Existing Features" or "Existing Ideas" sections below + +${contextSection}${existingWorkSection}`; + } + + /** + * Parse AI response into structured suggestions + */ + private parseSuggestionsFromResponse( + response: string, + category: IdeaCategory + ): AnalysisSuggestion[] { + try { + // Try to extract JSON from the response + const jsonMatch = response.match(/\[[\s\S]*\]/); + if (!jsonMatch) { + logger.warn('No JSON array found in response, falling back to text parsing'); + return this.parseTextResponse(response, category); + } + + const parsed = JSON.parse(jsonMatch[0]); + if (!Array.isArray(parsed)) { + return this.parseTextResponse(response, category); + } + + return parsed.map((item: any, index: number) => ({ + id: this.generateId('sug'), + category, + title: item.title || `Suggestion ${index + 1}`, + description: item.description || '', + rationale: item.rationale || '', + priority: item.priority || 'medium', + relatedFiles: item.relatedFiles || [], + })); + } catch (error) { + logger.warn('Failed to parse JSON response:', error); + return this.parseTextResponse(response, category); + } + } + + /** + * Fallback: parse text response into suggestions + */ + private parseTextResponse(response: string, category: IdeaCategory): AnalysisSuggestion[] { + const suggestions: AnalysisSuggestion[] = []; + + // Try to find numbered items or headers + const lines = response.split('\n'); + let currentSuggestion: Partial | null = null; + let currentContent: string[] = []; + + for (const line of lines) { + // Check for numbered items or markdown headers + const titleMatch = line.match(/^(?:\d+[\.\)]\s*\*{0,2}|#{1,3}\s+)(.+)/); + + if (titleMatch) { + // Save previous suggestion + if (currentSuggestion && currentSuggestion.title) { + suggestions.push({ + id: this.generateId('sug'), + category, + title: currentSuggestion.title, + description: currentContent.join(' ').trim() || currentSuggestion.title, + rationale: '', + priority: 'medium', + ...currentSuggestion, + } as AnalysisSuggestion); + } + + // Start new suggestion + currentSuggestion = { + title: titleMatch[1].replace(/\*{1,2}/g, '').trim(), + }; + currentContent = []; + } else if (currentSuggestion && line.trim()) { + currentContent.push(line.trim()); + } + } + + // Don't forget the last suggestion + if (currentSuggestion && currentSuggestion.title) { + suggestions.push({ + id: this.generateId('sug'), + category, + title: currentSuggestion.title, + description: currentContent.join(' ').trim() || currentSuggestion.title, + rationale: '', + priority: 'medium', + } as AnalysisSuggestion); + } + + // If no suggestions found, create one from the whole response + if (suggestions.length === 0 && response.trim()) { + suggestions.push({ + id: this.generateId('sug'), + category, + title: 'AI Suggestion', + description: response.slice(0, 500), + rationale: '', + priority: 'medium', + }); + } + + return suggestions.slice(0, 5); // Max 5 suggestions + } + + // ============================================================================ + // Guided Prompts + // ============================================================================ + + /** + * Get all prompt categories + */ + getPromptCategories(): PromptCategory[] { + return [ + { + id: 'feature', + name: 'Features', + icon: 'Zap', + description: 'New capabilities and functionality', + }, + { + id: 'ux-ui', + name: 'UX/UI', + icon: 'Palette', + description: 'Design and user experience improvements', + }, + { + id: 'dx', + name: 'Developer Experience', + icon: 'Code', + description: 'Developer tooling and workflows', + }, + { + id: 'growth', + name: 'Growth', + icon: 'TrendingUp', + description: 'User engagement and retention', + }, + { + id: 'technical', + name: 'Technical', + icon: 'Cpu', + description: 'Architecture and infrastructure', + }, + ]; + } + + /** + * Get prompts for a specific category + */ + getPromptsByCategory(category: IdeaCategory): IdeationPrompt[] { + const allPrompts = this.getAllPrompts(); + return allPrompts.filter((p) => p.category === category); + } + + /** + * Get all guided prompts + * NOTE: Keep in sync with apps/ui/src/components/views/ideation-view/data/guided-prompts.ts + */ + getAllPrompts(): IdeationPrompt[] { + return [ + // Feature prompts + { + id: 'feature-missing', + category: 'feature', + title: 'Missing Features', + description: 'Discover features users might expect', + prompt: + "Based on the project context provided, identify features that users of similar applications typically expect but might be missing. Consider the app's domain, target users, and common patterns in similar products.", + }, + { + id: 'feature-automation', + category: 'feature', + title: 'Automation Opportunities', + description: 'Find manual processes that could be automated', + prompt: + 'Based on the project context, identify manual processes or repetitive tasks that could be automated. Look for patterns where users might be doing things repeatedly that software could handle.', + }, + { + id: 'feature-integrations', + category: 'feature', + title: 'Integration Ideas', + description: 'Identify valuable third-party integrations', + prompt: + "Based on the project context, what third-party services or APIs would provide value if integrated? Consider the app's domain and what complementary services users might need.", + }, + { + id: 'feature-workflow', + category: 'feature', + title: 'Workflow Improvements', + description: 'Streamline user workflows', + prompt: + 'Based on the project context, analyze the user workflows. What steps could be combined, eliminated, or automated? Where are users likely spending too much time on repetitive tasks?', + }, + + // UX/UI prompts + { + id: 'ux-friction', + category: 'ux-ui', + title: 'Friction Points', + description: 'Identify where users might get stuck', + prompt: + 'Based on the project context, identify potential user friction points. Where might users get confused, stuck, or frustrated? Consider form submissions, navigation, error states, and complex interactions.', + }, + { + id: 'ux-empty-states', + category: 'ux-ui', + title: 'Empty States', + description: 'Improve empty state experiences', + prompt: + "Based on the project context, identify empty states that could be improved. How can we guide users when there's no content? Consider onboarding, helpful prompts, and sample data.", + }, + { + id: 'ux-accessibility', + category: 'ux-ui', + title: 'Accessibility Improvements', + description: 'Enhance accessibility and inclusivity', + prompt: + 'Based on the project context, suggest accessibility improvements. Consider keyboard navigation, screen reader support, color contrast, focus states, and ARIA labels. What specific improvements would make this more accessible?', + }, + { + id: 'ux-mobile', + category: 'ux-ui', + title: 'Mobile Experience', + description: 'Optimize for mobile users', + prompt: + 'Based on the project context, suggest improvements for the mobile user experience. Consider touch targets, responsive layouts, and mobile-specific interactions.', + }, + { + id: 'ux-feedback', + category: 'ux-ui', + title: 'User Feedback', + description: 'Improve feedback and status indicators', + prompt: + 'Based on the project context, analyze how the application communicates with users. Where are loading states, success messages, or error handling missing or unclear? What feedback would help users understand what is happening?', + }, + + // DX prompts + { + id: 'dx-documentation', + category: 'dx', + title: 'Documentation Gaps', + description: 'Identify missing documentation', + prompt: + 'Based on the project context, identify areas that could benefit from better documentation. What would help new developers understand the architecture, APIs, and conventions? Consider inline comments, READMEs, and API docs.', + }, + { + id: 'dx-testing', + category: 'dx', + title: 'Testing Improvements', + description: 'Enhance test coverage and quality', + prompt: + 'Based on the project context, suggest areas that need better test coverage. What types of tests might be missing? Consider unit tests, integration tests, and end-to-end tests.', + }, + { + id: 'dx-tooling', + category: 'dx', + title: 'Developer Tooling', + description: 'Improve development workflows', + prompt: + 'Based on the project context, suggest improvements to development workflows. What improvements would speed up development? Consider build times, hot reload, debugging tools, and developer scripts.', + }, + { + id: 'dx-error-handling', + category: 'dx', + title: 'Error Handling', + description: 'Improve error messages and debugging', + prompt: + 'Based on the project context, analyze error handling. Where are error messages unclear or missing? What would help developers debug issues faster? Consider logging, error boundaries, and stack traces.', + }, + + // Growth prompts + { + id: 'growth-onboarding', + category: 'growth', + title: 'Onboarding Flow', + description: 'Improve new user experience', + prompt: + 'Based on the project context, suggest improvements to the onboarding experience. How can we help new users understand the value and get started quickly? Consider tutorials, progressive disclosure, and quick wins.', + }, + { + id: 'growth-engagement', + category: 'growth', + title: 'User Engagement', + description: 'Increase user retention and activity', + prompt: + 'Based on the project context, suggest features that would increase user engagement and retention. What would bring users back daily? Consider notifications, streaks, social features, and personalization.', + }, + { + id: 'growth-sharing', + category: 'growth', + title: 'Shareability', + description: 'Make the app more shareable', + prompt: + 'Based on the project context, suggest ways to make the application more shareable. What features would encourage users to invite others or share their work? Consider collaboration, public profiles, and export features.', + }, + { + id: 'growth-monetization', + category: 'growth', + title: 'Monetization Ideas', + description: 'Identify potential revenue streams', + prompt: + 'Based on the project context, what features or tiers could support monetization? Consider premium features, usage limits, team features, and integrations that users would pay for.', + }, + + // Technical prompts + { + id: 'tech-performance', + category: 'technical', + title: 'Performance Optimization', + description: 'Identify performance bottlenecks', + prompt: + 'Based on the project context, suggest performance optimization opportunities. Where might bottlenecks exist? Consider database queries, API calls, bundle size, rendering, and caching strategies.', + }, + { + id: 'tech-architecture', + category: 'technical', + title: 'Architecture Review', + description: 'Evaluate and improve architecture', + prompt: + 'Based on the project context, suggest architectural improvements. What would make the codebase more maintainable, scalable, or testable? Consider separation of concerns, dependency management, and patterns.', + }, + { + id: 'tech-debt', + category: 'technical', + title: 'Technical Debt', + description: 'Identify areas needing refactoring', + prompt: + 'Based on the project context, identify potential technical debt. What areas might be becoming hard to maintain or understand? What refactoring would have the highest impact? Consider duplicated code, complexity, and outdated patterns.', + }, + { + id: 'tech-security', + category: 'technical', + title: 'Security Review', + description: 'Identify security improvements', + prompt: + 'Based on the project context, review for security improvements. What best practices are missing? Consider authentication, authorization, input validation, and data protection. Note: This is for improvement suggestions, not a security audit.', + }, + + // Security prompts + { + id: 'security-auth', + category: 'security', + title: 'Authentication Security', + description: 'Review authentication mechanisms', + prompt: + 'Based on the project context, analyze the authentication system. What security improvements would strengthen user authentication? Consider password policies, session management, MFA, and token handling.', + }, + { + id: 'security-data', + category: 'security', + title: 'Data Protection', + description: 'Protect sensitive user data', + prompt: + 'Based on the project context, review how sensitive data is handled. What improvements would better protect user privacy? Consider encryption, data minimization, secure storage, and data retention policies.', + }, + { + id: 'security-input', + category: 'security', + title: 'Input Validation', + description: 'Prevent injection attacks', + prompt: + 'Based on the project context, analyze input handling. Where could input validation be strengthened? Consider SQL injection, XSS, command injection, and file upload vulnerabilities.', + }, + { + id: 'security-api', + category: 'security', + title: 'API Security', + description: 'Secure API endpoints', + prompt: + 'Based on the project context, review API security. What improvements would make the API more secure? Consider rate limiting, authorization, CORS, and request validation.', + }, + + // Performance prompts + { + id: 'perf-frontend', + category: 'performance', + title: 'Frontend Performance', + description: 'Optimize UI rendering and loading', + prompt: + 'Based on the project context, analyze frontend performance. What optimizations would improve load times and responsiveness? Consider bundle splitting, lazy loading, memoization, and render optimization.', + }, + { + id: 'perf-backend', + category: 'performance', + title: 'Backend Performance', + description: 'Optimize server-side operations', + prompt: + 'Based on the project context, review backend performance. What optimizations would improve response times? Consider database queries, caching strategies, async operations, and resource pooling.', + }, + { + id: 'perf-database', + category: 'performance', + title: 'Database Optimization', + description: 'Improve query performance', + prompt: + 'Based on the project context, analyze database interactions. What optimizations would improve data access performance? Consider indexing, query optimization, denormalization, and connection pooling.', + }, + { + id: 'perf-caching', + category: 'performance', + title: 'Caching Strategies', + description: 'Implement effective caching', + prompt: + 'Based on the project context, review caching opportunities. Where would caching provide the most benefit? Consider API responses, computed values, static assets, and session data.', + }, + + // Accessibility prompts + { + id: 'a11y-keyboard', + category: 'accessibility', + title: 'Keyboard Navigation', + description: 'Enable full keyboard access', + prompt: + 'Based on the project context, analyze keyboard accessibility. What improvements would enable users to navigate entirely with keyboard? Consider focus management, tab order, and keyboard shortcuts.', + }, + { + id: 'a11y-screen-reader', + category: 'accessibility', + title: 'Screen Reader Support', + description: 'Improve screen reader experience', + prompt: + 'Based on the project context, review screen reader compatibility. What improvements would help users with visual impairments? Consider ARIA labels, semantic HTML, live regions, and alt text.', + }, + { + id: 'a11y-visual', + category: 'accessibility', + title: 'Visual Accessibility', + description: 'Improve visual design for all users', + prompt: + 'Based on the project context, analyze visual accessibility. What improvements would help users with visual impairments? Consider color contrast, text sizing, focus indicators, and reduced motion.', + }, + { + id: 'a11y-forms', + category: 'accessibility', + title: 'Accessible Forms', + description: 'Make forms usable for everyone', + prompt: + 'Based on the project context, review form accessibility. What improvements would make forms more accessible? Consider labels, error messages, required field indicators, and input assistance.', + }, + + // Analytics prompts + { + id: 'analytics-tracking', + category: 'analytics', + title: 'User Tracking', + description: 'Track key user behaviors', + prompt: + 'Based on the project context, analyze analytics opportunities. What user behaviors should be tracked to understand engagement? Consider page views, feature usage, conversion funnels, and session duration.', + }, + { + id: 'analytics-metrics', + category: 'analytics', + title: 'Key Metrics', + description: 'Define success metrics', + prompt: + 'Based on the project context, what key metrics should be tracked? Consider user acquisition, retention, engagement, and feature adoption. What dashboards would be most valuable?', + }, + { + id: 'analytics-errors', + category: 'analytics', + title: 'Error Monitoring', + description: 'Track and analyze errors', + prompt: + 'Based on the project context, review error handling for monitoring opportunities. What error tracking would help identify and fix issues faster? Consider error aggregation, alerting, and stack traces.', + }, + { + id: 'analytics-performance', + category: 'analytics', + title: 'Performance Monitoring', + description: 'Track application performance', + prompt: + 'Based on the project context, analyze performance monitoring opportunities. What metrics would help identify bottlenecks? Consider load times, API response times, and resource usage.', + }, + ]; + } + + // ============================================================================ + // Private Helpers + // ============================================================================ + + private buildIdeationSystemPrompt( + contextFilesPrompt: string | undefined, + category?: IdeaCategory, + existingWorkContext?: string + ): string { + const basePrompt = `You are an AI product strategist and UX expert helping brainstorm ideas for improving a software project. + +Your role is to: +- Analyze the codebase structure and patterns +- Identify opportunities for improvement +- Suggest actionable ideas with clear rationale +- Consider user experience, technical feasibility, and business value +- Be specific and reference actual files/components when possible + +When suggesting ideas: +1. Provide a clear, concise title +2. Explain the problem or opportunity +3. Describe the proposed solution +4. Highlight the expected benefit +5. Note any dependencies or considerations + +IMPORTANT: Do NOT suggest features or ideas that already exist in the project. Check the "Existing Features" and "Existing Ideas" sections below to avoid duplicates. + +Focus on practical, implementable suggestions that would genuinely improve the product.`; + + const categoryContext = category + ? `\n\nFocus area: ${this.getCategoryDescription(category)}` + : ''; + + const contextSection = contextFilesPrompt + ? `\n\n## Project Context\n${contextFilesPrompt}` + : ''; + + const existingWorkSection = existingWorkContext ? `\n\n${existingWorkContext}` : ''; + + return basePrompt + categoryContext + contextSection + existingWorkSection; + } + + private getCategoryDescription(category: IdeaCategory): string { + const descriptions: Record = { + feature: 'New features and capabilities that add value for users', + 'ux-ui': 'User interface and user experience improvements', + dx: 'Developer experience and tooling improvements', + growth: 'User acquisition, engagement, and retention', + technical: 'Architecture, performance, and infrastructure', + security: 'Security improvements and vulnerability fixes', + performance: 'Performance optimization and speed improvements', + accessibility: 'Accessibility features and inclusive design', + analytics: 'Analytics, monitoring, and insights features', + }; + return descriptions[category] || ''; + } + + /** + * Gather basic project information for context when no context files exist + */ + private async gatherBasicProjectInfo(projectPath: string): Promise { + const parts: string[] = []; + + // Try to read package.json + try { + const packageJsonPath = path.join(projectPath, 'package.json'); + const content = (await secureFs.readFile(packageJsonPath, 'utf-8')) as string; + const pkg = JSON.parse(content); + + parts.push('## Project Information (from package.json)'); + if (pkg.name) parts.push(`**Name:** ${pkg.name}`); + if (pkg.description) parts.push(`**Description:** ${pkg.description}`); + if (pkg.version) parts.push(`**Version:** ${pkg.version}`); + + const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; + const depNames = Object.keys(allDeps); + + // Detect framework and language + let framework = 'Unknown'; + if (allDeps.react) framework = allDeps.next ? 'Next.js' : 'React'; + else if (allDeps.vue) framework = allDeps.nuxt ? 'Nuxt' : 'Vue'; + else if (allDeps['@angular/core']) framework = 'Angular'; + else if (allDeps.svelte) framework = 'Svelte'; + else if (allDeps.express) framework = 'Express'; + else if (allDeps.fastify) framework = 'Fastify'; + else if (allDeps.koa) framework = 'Koa'; + + const language = allDeps.typescript ? 'TypeScript' : 'JavaScript'; + parts.push(`**Tech Stack:** ${framework} with ${language}`); + + // Key dependencies + const keyDeps = depNames + .filter( + (d) => !d.startsWith('@types/') && !['typescript', 'eslint', 'prettier'].includes(d) + ) + .slice(0, 15); + if (keyDeps.length > 0) { + parts.push(`**Key Dependencies:** ${keyDeps.join(', ')}`); + } + + // Scripts + if (pkg.scripts) { + const scriptNames = Object.keys(pkg.scripts).slice(0, 10); + parts.push(`**Available Scripts:** ${scriptNames.join(', ')}`); + } + } catch { + // No package.json, try other files + } + + // Try to read README.md (first 500 chars) + try { + const readmePath = path.join(projectPath, 'README.md'); + const content = (await secureFs.readFile(readmePath, 'utf-8')) as string; + if (content) { + parts.push('\n## README.md (excerpt)'); + parts.push(content.slice(0, 1000)); + } + } catch { + // No README + } + + // Try to get cached analysis + const cachedAnalysis = await this.getCachedAnalysis(projectPath); + if (cachedAnalysis) { + parts.push('\n## Project Structure Analysis'); + parts.push(cachedAnalysis.summary || ''); + if (cachedAnalysis.routes && cachedAnalysis.routes.length > 0) { + parts.push(`**Routes:** ${cachedAnalysis.routes.map((r) => r.name).join(', ')}`); + } + if (cachedAnalysis.components && cachedAnalysis.components.length > 0) { + parts.push( + `**Components:** ${cachedAnalysis.components + .slice(0, 10) + .map((c) => c.name) + .join( + ', ' + )}${cachedAnalysis.components.length > 10 ? ` and ${cachedAnalysis.components.length - 10} more` : ''}` + ); + } + } + + if (parts.length === 0) { + return null; + } + + return parts.join('\n'); + } + + /** + * Gather existing features and ideas to prevent duplicate suggestions + * Returns a concise list of titles grouped by status to avoid polluting context + */ + private async gatherExistingWorkContext(projectPath: string): Promise { + const parts: string[] = []; + + // Load existing features from the board + if (this.featureLoader) { + try { + const features = await this.featureLoader.getAll(projectPath); + if (features.length > 0) { + parts.push('## Existing Features (Do NOT regenerate these)'); + parts.push( + 'The following features already exist on the board. Do NOT suggest similar ideas:\n' + ); + + // Group features by status for clarity + const byStatus: Record = { + done: [], + 'in-review': [], + 'in-progress': [], + backlog: [], + }; + + for (const feature of features) { + const status = feature.status || 'backlog'; + const title = feature.title || 'Untitled'; + if (byStatus[status]) { + byStatus[status].push(title); + } else { + byStatus['backlog'].push(title); + } + } + + // Output completed features first (most important to not duplicate) + if (byStatus['done'].length > 0) { + parts.push(`**Completed:** ${byStatus['done'].join(', ')}`); + } + if (byStatus['in-review'].length > 0) { + parts.push(`**In Review:** ${byStatus['in-review'].join(', ')}`); + } + if (byStatus['in-progress'].length > 0) { + parts.push(`**In Progress:** ${byStatus['in-progress'].join(', ')}`); + } + if (byStatus['backlog'].length > 0) { + parts.push(`**Backlog:** ${byStatus['backlog'].join(', ')}`); + } + parts.push(''); + } + } catch (error) { + logger.warn('Failed to load existing features:', error); + } + } + + // Load existing ideas + try { + const ideas = await this.getIdeas(projectPath); + // Filter out archived ideas + const activeIdeas = ideas.filter((idea) => idea.status !== 'archived'); + + if (activeIdeas.length > 0) { + parts.push('## Existing Ideas (Do NOT regenerate these)'); + parts.push( + 'The following ideas have already been captured. Do NOT suggest similar ideas:\n' + ); + + // Group by category for organization + const byCategory: Record = {}; + for (const idea of activeIdeas) { + const cat = idea.category || 'feature'; + if (!byCategory[cat]) { + byCategory[cat] = []; + } + byCategory[cat].push(idea.title); + } + + for (const [category, titles] of Object.entries(byCategory)) { + parts.push(`**${category}:** ${titles.join(', ')}`); + } + parts.push(''); + } + } catch (error) { + logger.warn('Failed to load existing ideas:', error); + } + + return parts.join('\n'); + } + + private async gatherProjectStructure(projectPath: string): Promise<{ + totalFiles: number; + routes: AnalysisFileInfo[]; + components: AnalysisFileInfo[]; + services: AnalysisFileInfo[]; + framework?: string; + language?: string; + dependencies?: string[]; + }> { + const routes: AnalysisFileInfo[] = []; + const components: AnalysisFileInfo[] = []; + const services: AnalysisFileInfo[] = []; + let totalFiles = 0; + let framework: string | undefined; + let language: string | undefined; + const dependencies: string[] = []; + + // Check for package.json to detect framework and dependencies + try { + const packageJsonPath = path.join(projectPath, 'package.json'); + const content = (await secureFs.readFile(packageJsonPath, 'utf-8')) as string; + const pkg = JSON.parse(content); + + const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; + dependencies.push(...Object.keys(allDeps).slice(0, 20)); // Top 20 deps + + if (allDeps.react) framework = 'React'; + else if (allDeps.vue) framework = 'Vue'; + else if (allDeps.angular) framework = 'Angular'; + else if (allDeps.next) framework = 'Next.js'; + else if (allDeps.express) framework = 'Express'; + + language = allDeps.typescript ? 'TypeScript' : 'JavaScript'; + } catch { + // No package.json + } + + // Scan common directories + const scanPatterns = [ + { dir: 'src/routes', type: 'route' as const }, + { dir: 'src/pages', type: 'route' as const }, + { dir: 'app', type: 'route' as const }, + { dir: 'src/components', type: 'component' as const }, + { dir: 'components', type: 'component' as const }, + { dir: 'src/services', type: 'service' as const }, + { dir: 'src/lib', type: 'service' as const }, + { dir: 'lib', type: 'service' as const }, + ]; + + for (const pattern of scanPatterns) { + const fullPath = path.join(projectPath, pattern.dir); + try { + const files = await this.scanDirectory(fullPath, pattern.type); + totalFiles += files.length; + + if (pattern.type === 'route') routes.push(...files); + else if (pattern.type === 'component') components.push(...files); + else if (pattern.type === 'service') services.push(...files); + } catch { + // Directory doesn't exist + } + } + + return { + totalFiles, + routes: routes.slice(0, 20), + components: components.slice(0, 30), + services: services.slice(0, 20), + framework, + language, + dependencies, + }; + } + + private async scanDirectory( + dirPath: string, + type: 'route' | 'component' | 'service' | 'model' | 'config' | 'test' | 'other' + ): Promise { + const results: AnalysisFileInfo[] = []; + + try { + const entries = (await secureFs.readdir(dirPath, { withFileTypes: true })) as any[]; + + for (const entry of entries) { + if (entry.isDirectory()) { + const subResults = await this.scanDirectory(path.join(dirPath, entry.name), type); + results.push(...subResults); + } else if (entry.isFile() && this.isCodeFile(entry.name)) { + results.push({ + path: path.join(dirPath, entry.name), + type, + name: entry.name.replace(/\.(tsx?|jsx?|vue)$/, ''), + }); + } + } + } catch { + // Ignore errors + } + + return results; + } + + private isCodeFile(filename: string): boolean { + return ( + /\.(tsx?|jsx?|vue|svelte)$/.test(filename) && + !filename.includes('.test.') && + !filename.includes('.spec.') + ); + } + + private async generateAnalysisSuggestions( + _projectPath: string, + structure: Awaited> + ): Promise { + // Generate basic suggestions based on project structure analysis + const suggestions: AnalysisSuggestion[] = []; + + if (structure.routes.length > 0 && structure.routes.length < 5) { + suggestions.push({ + id: this.generateId('sug'), + category: 'feature', + title: 'Expand Core Functionality', + description: 'The app has a small number of routes. Consider adding more features.', + rationale: `Only ${structure.routes.length} routes detected. Most apps benefit from additional navigation options.`, + priority: 'medium', + }); + } + + if ( + !structure.dependencies?.includes('react-query') && + !structure.dependencies?.includes('@tanstack/react-query') + ) { + suggestions.push({ + id: this.generateId('sug'), + category: 'technical', + title: 'Add Data Fetching Library', + description: 'Consider adding React Query or similar for better data management.', + rationale: + 'Data fetching libraries provide caching, background updates, and better loading states.', + priority: 'low', + }); + } + + return suggestions; + } + + private generateAnalysisSummary( + structure: Awaited>, + suggestions: AnalysisSuggestion[] + ): string { + const parts: string[] = []; + + if (structure.framework) { + parts.push(`${structure.framework} ${structure.language || ''} application`); + } + + parts.push(`with ${structure.totalFiles} code files`); + parts.push(`${structure.routes.length} routes`); + parts.push(`${structure.components.length} components`); + parts.push(`${structure.services.length} services`); + + const summary = parts.join(', '); + const highPriority = suggestions.filter((s) => s.priority === 'high').length; + + return `${summary}. Found ${suggestions.length} improvement opportunities${highPriority > 0 ? ` (${highPriority} high priority)` : ''}.`; + } + + private mapIdeaCategoryToFeatureCategory(category: IdeaCategory): string { + const mapping: Record = { + feature: 'feature', + 'ux-ui': 'enhancement', + dx: 'chore', + growth: 'feature', + technical: 'refactor', + security: 'bug', + performance: 'enhancement', + accessibility: 'enhancement', + analytics: 'feature', + }; + return mapping[category] || 'feature'; + } + + private async saveSessionToDisk( + projectPath: string, + session: IdeationSession, + messages: IdeationMessage[] + ): Promise { + await secureFs.mkdir(getIdeationSessionsDir(projectPath), { recursive: true }); + const data = { session, messages }; + await secureFs.writeFile( + getIdeationSessionPath(projectPath, session.id), + JSON.stringify(data, null, 2), + 'utf-8' + ); + } + + private async loadSessionFromDisk( + projectPath: string, + sessionId: string + ): Promise<{ session: IdeationSession; messages: IdeationMessage[] } | null> { + try { + const content = (await secureFs.readFile( + getIdeationSessionPath(projectPath, sessionId), + 'utf-8' + )) as string; + return JSON.parse(content); + } catch { + return null; + } + } + + private generateId(prefix: string): string { + return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + } +} diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts index de4e19f8..314765e0 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-navigation.ts @@ -10,6 +10,7 @@ import { CircleDot, GitPullRequest, Zap, + Lightbulb, } from 'lucide-react'; import type { NavSection, NavItem } from '../types'; import type { KeyboardShortcut } from '@/hooks/use-keyboard-shortcuts'; @@ -30,6 +31,9 @@ interface UseNavigationProps { agent: string; terminal: string; settings: string; + ideation: string; + githubIssues: string; + githubPrs: string; }; hideSpecEditor: boolean; hideContext: boolean; @@ -92,6 +96,12 @@ export function useNavigation({ // Build navigation sections const navSections: NavSection[] = useMemo(() => { const allToolsItems: NavItem[] = [ + { + id: 'ideation', + label: 'Ideation', + icon: Lightbulb, + shortcut: shortcuts.ideation, + }, { id: 'spec', label: 'Spec Editor', @@ -172,12 +182,14 @@ export function useNavigation({ id: 'github-issues', label: 'Issues', icon: CircleDot, + shortcut: shortcuts.githubIssues, count: unviewedValidationsCount, }, { id: 'github-prs', label: 'Pull Requests', icon: GitPullRequest, + shortcut: shortcuts.githubPrs, }, ], }); diff --git a/apps/ui/src/components/ui/keyboard-map.tsx b/apps/ui/src/components/ui/keyboard-map.tsx index 8f235d55..2e00c1e2 100644 --- a/apps/ui/src/components/ui/keyboard-map.tsx +++ b/apps/ui/src/components/ui/keyboard-map.tsx @@ -90,6 +90,9 @@ const SHORTCUT_LABELS: Record = { settings: 'Settings', profiles: 'AI Profiles', terminal: 'Terminal', + ideation: 'Ideation', + githubIssues: 'GitHub Issues', + githubPrs: 'Pull Requests', toggleSidebar: 'Toggle Sidebar', addFeature: 'Add Feature', addContextFile: 'Add Context File', @@ -115,6 +118,9 @@ const SHORTCUT_CATEGORIES: Record setShowSuggestionsDialog(true)} - suggestionsCount={suggestionsCount} onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)} pipelineConfig={ currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null @@ -1269,17 +1249,6 @@ export function BoardView() { isMaximized={isMaximized} /> - {/* Feature Suggestions Dialog */} - - {/* Backlog Plan Dialog */} void; - projectPath: string; - // Props to persist state across dialog open/close - suggestions: FeatureSuggestion[]; - setSuggestions: (suggestions: FeatureSuggestion[]) => void; - isGenerating: boolean; - setIsGenerating: (generating: boolean) => void; -} - -// Configuration for each suggestion type -const suggestionTypeConfig: Record< - SuggestionType, - { - label: string; - icon: React.ComponentType<{ className?: string }>; - description: string; - color: string; - } -> = { - features: { - label: 'Feature Suggestions', - icon: Lightbulb, - description: 'Discover missing features and improvements', - color: 'text-yellow-500', - }, - refactoring: { - label: 'Refactoring Suggestions', - icon: RefreshCw, - description: 'Find code smells and refactoring opportunities', - color: 'text-blue-500', - }, - security: { - label: 'Security Suggestions', - icon: Shield, - description: 'Identify security vulnerabilities and issues', - color: 'text-red-500', - }, - performance: { - label: 'Performance Suggestions', - icon: Zap, - description: 'Discover performance bottlenecks and optimizations', - color: 'text-green-500', - }, -}; - -export function FeatureSuggestionsDialog({ - open, - onClose, - projectPath, - suggestions, - setSuggestions, - isGenerating, - setIsGenerating, -}: FeatureSuggestionsDialogProps) { - const [progress, setProgress] = useState([]); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [expandedIds, setExpandedIds] = useState>(new Set()); - const [isImporting, setIsImporting] = useState(false); - const [currentSuggestionType, setCurrentSuggestionType] = useState(null); - const [viewMode, setViewMode] = useState<'parsed' | 'raw'>('parsed'); - const scrollRef = useRef(null); - const autoScrollRef = useRef(true); - - const { features, setFeatures } = useAppStore(); - - // Initialize selectedIds when suggestions change - useEffect(() => { - if (suggestions.length > 0 && selectedIds.size === 0) { - setSelectedIds(new Set(suggestions.map((s) => s.id))); - } - }, [suggestions, selectedIds.size]); - - // Auto-scroll progress when new content arrives - useEffect(() => { - if (autoScrollRef.current && scrollRef.current && isGenerating) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [progress, isGenerating]); - - // Listen for suggestion events when dialog is open - useEffect(() => { - if (!open) return; - - const api = getElectronAPI(); - if (!api?.suggestions) return; - - const unsubscribe = api.suggestions.onEvent((event: SuggestionsEvent) => { - if (event.type === 'suggestions_progress') { - setProgress((prev) => [...prev, event.content || '']); - } else if (event.type === 'suggestions_tool') { - const toolName = event.tool || 'Unknown Tool'; - const toolInput = event.input ? JSON.stringify(event.input, null, 2) : ''; - const formattedTool = `\n🔧 Tool: ${toolName}\n${toolInput ? `Input: ${toolInput}\n` : ''}`; - setProgress((prev) => [...prev, formattedTool]); - } else if (event.type === 'suggestions_complete') { - setIsGenerating(false); - if (event.suggestions && event.suggestions.length > 0) { - setSuggestions(event.suggestions); - // Select all by default - setSelectedIds(new Set(event.suggestions.map((s) => s.id))); - const typeLabel = currentSuggestionType - ? suggestionTypeConfig[currentSuggestionType].label.toLowerCase() - : 'suggestions'; - toast.success(`Generated ${event.suggestions.length} ${typeLabel}!`); - } else { - toast.info('No suggestions generated. Try again.'); - } - } else if (event.type === 'suggestions_error') { - setIsGenerating(false); - toast.error(`Error: ${event.error}`); - } - }); - - return () => { - unsubscribe(); - }; - }, [open, setSuggestions, setIsGenerating, currentSuggestionType]); - - // Start generating suggestions for a specific type - const handleGenerate = useCallback( - async (suggestionType: SuggestionType) => { - const api = getElectronAPI(); - if (!api?.suggestions) { - toast.error('Suggestions API not available'); - return; - } - - setIsGenerating(true); - setProgress([]); - setSuggestions([]); - setSelectedIds(new Set()); - setCurrentSuggestionType(suggestionType); - - try { - const result = await api.suggestions.generate(projectPath, suggestionType); - if (!result.success) { - toast.error(result.error || 'Failed to start generation'); - setIsGenerating(false); - } - } catch (error) { - console.error('Failed to generate suggestions:', error); - toast.error('Failed to start generation'); - setIsGenerating(false); - } - }, - [projectPath, setIsGenerating, setSuggestions] - ); - - // Stop generating - const handleStop = useCallback(async () => { - const api = getElectronAPI(); - if (!api?.suggestions) return; - - try { - await api.suggestions.stop(); - setIsGenerating(false); - toast.info('Generation stopped'); - } catch (error) { - console.error('Failed to stop generation:', error); - } - }, [setIsGenerating]); - - // Toggle suggestion selection - const toggleSelection = useCallback((id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - // Toggle expand/collapse for a suggestion - const toggleExpanded = useCallback((id: string) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }, []); - - // Select/deselect all - const toggleSelectAll = useCallback(() => { - if (selectedIds.size === suggestions.length) { - setSelectedIds(new Set()); - } else { - setSelectedIds(new Set(suggestions.map((s) => s.id))); - } - }, [selectedIds.size, suggestions]); - - // Import selected suggestions as features - const handleImport = useCallback(async () => { - if (selectedIds.size === 0) { - toast.warning('No suggestions selected'); - return; - } - - setIsImporting(true); - - try { - const api = getElectronAPI(); - const selectedSuggestions = suggestions.filter((s) => selectedIds.has(s.id)); - - // Create new features from selected suggestions - const newFeatures: Feature[] = selectedSuggestions.map((s) => ({ - id: `feature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - category: s.category, - description: s.description, - steps: [], // Required empty steps array for new features - status: 'backlog' as const, - skipTests: true, // As specified, testing mode true - priority: s.priority, // Preserve priority from suggestion - })); - - // Create each new feature using the features API - if (api.features) { - for (const feature of newFeatures) { - await api.features.create(projectPath, feature); - } - } - - // Merge with existing features for store update - const updatedFeatures = [...features, ...newFeatures]; - - // Update store - setFeatures(updatedFeatures); - - toast.success(`Imported ${newFeatures.length} features to backlog!`); - - // Clear suggestions after importing - setSuggestions([]); - setSelectedIds(new Set()); - setProgress([]); - setCurrentSuggestionType(null); - - onClose(); - } catch (error) { - console.error('Failed to import features:', error); - toast.error('Failed to import features'); - } finally { - setIsImporting(false); - } - }, [selectedIds, suggestions, features, setFeatures, setSuggestions, projectPath, onClose]); - - // Handle scroll to detect if user scrolled up - const handleScroll = () => { - if (!scrollRef.current) return; - - const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; - const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; - autoScrollRef.current = isAtBottom; - }; - - // Go back to type selection - const handleBackToSelection = useCallback(() => { - setSuggestions([]); - setSelectedIds(new Set()); - setProgress([]); - setCurrentSuggestionType(null); - }, [setSuggestions]); - - const hasStarted = isGenerating || progress.length > 0 || suggestions.length > 0; - const hasSuggestions = suggestions.length > 0; - const currentConfig = currentSuggestionType ? suggestionTypeConfig[currentSuggestionType] : null; - - return ( - - - - - {currentConfig ? ( - <> - - {currentConfig.label} - - ) : ( - <> - - AI Suggestions - - )} - - - {currentConfig - ? currentConfig.description - : 'Analyze your project to discover improvements. Choose a suggestion type below.'} - - - - {!hasStarted ? ( - // Initial state - show suggestion type buttons -
-

- Our AI will analyze your project and generate actionable suggestions. Choose what type - of analysis you want to perform: -

-
- {( - Object.entries(suggestionTypeConfig) as [ - SuggestionType, - (typeof suggestionTypeConfig)[SuggestionType], - ][] - ).map(([type, config]) => { - const Icon = config.icon; - return ( - - ); - })} -
-
- ) : isGenerating ? ( - // Generating state - show progress -
-
-
- - Analyzing project... -
-
-
- - -
- -
-
-
- {progress.length === 0 ? ( -
- - Waiting for AI response... -
- ) : viewMode === 'parsed' ? ( - - ) : ( -
- {progress.join('')} -
- )} -
-
- ) : hasSuggestions ? ( - // Results state - show suggestions list -
-
-
- - {suggestions.length} suggestions generated - - -
- {selectedIds.size} selected -
- -
- {suggestions.map((suggestion) => { - const isSelected = selectedIds.has(suggestion.id); - const isExpanded = expandedIds.has(suggestion.id); - - return ( -
-
- toggleSelection(suggestion.id)} - className="mt-1" - /> -
-
- - - #{suggestion.priority} - - - {suggestion.category} - -
- - - {isExpanded && suggestion.reasoning && ( -
-

{suggestion.reasoning}

-
- )} -
-
-
- ); - })} -
-
- ) : ( - // No results state -
-

- No suggestions were generated. Try running the analysis again. -

-
- - {currentSuggestionType && ( - - )} -
-
- )} - - - {hasSuggestions && ( -
-
- - {currentSuggestionType && ( - - )} -
-
- - - {isImporting ? ( - - ) : ( - - )} - Import {selectedIds.size} Feature - {selectedIds.size !== 1 ? 's' : ''} - -
-
- )} - {!hasSuggestions && !isGenerating && hasStarted && ( - - )} -
-
-
- ); -} diff --git a/apps/ui/src/components/views/board-view/dialogs/index.ts b/apps/ui/src/components/views/board-view/dialogs/index.ts index 30f1df7e..6979f9d4 100644 --- a/apps/ui/src/components/views/board-view/dialogs/index.ts +++ b/apps/ui/src/components/views/board-view/dialogs/index.ts @@ -5,6 +5,5 @@ export { CompletedFeaturesModal } from './completed-features-modal'; export { ArchiveAllVerifiedDialog } from './archive-all-verified-dialog'; export { DeleteCompletedFeatureDialog } from './delete-completed-feature-dialog'; export { EditFeatureDialog } from './edit-feature-dialog'; -export { FeatureSuggestionsDialog } from './feature-suggestions-dialog'; export { FollowUpDialog } from './follow-up-dialog'; export { PlanApprovalDialog } from './plan-approval-dialog'; diff --git a/apps/ui/src/components/views/board-view/hooks/index.ts b/apps/ui/src/components/views/board-view/hooks/index.ts index a1577e07..9b855b06 100644 --- a/apps/ui/src/components/views/board-view/hooks/index.ts +++ b/apps/ui/src/components/views/board-view/hooks/index.ts @@ -7,4 +7,3 @@ export { useBoardEffects } from './use-board-effects'; export { useBoardBackground } from './use-board-background'; export { useBoardPersistence } from './use-board-persistence'; export { useFollowUpState } from './use-follow-up-state'; -export { useSuggestionsState } from './use-suggestions-state'; diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts index 318b326b..0603af3b 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-effects.ts @@ -6,9 +6,6 @@ interface UseBoardEffectsProps { currentProject: { path: string; id: string } | null; specCreatingForProject: string | null; setSpecCreatingForProject: (path: string | null) => void; - setSuggestionsCount: (count: number) => void; - setFeatureSuggestions: (suggestions: any[]) => void; - setIsGeneratingSuggestions: (generating: boolean) => void; checkContextExists: (featureId: string) => Promise; features: any[]; isLoading: boolean; @@ -20,9 +17,6 @@ export function useBoardEffects({ currentProject, specCreatingForProject, setSpecCreatingForProject, - setSuggestionsCount, - setFeatureSuggestions, - setIsGeneratingSuggestions, checkContextExists, features, isLoading, @@ -44,26 +38,6 @@ export function useBoardEffects({ }; }, [currentProject]); - // Listen for suggestions events to update count (persists even when dialog is closed) - useEffect(() => { - const api = getElectronAPI(); - if (!api?.suggestions) return; - - const unsubscribe = api.suggestions.onEvent((event) => { - if (event.type === 'suggestions_complete' && event.suggestions) { - setSuggestionsCount(event.suggestions.length); - setFeatureSuggestions(event.suggestions); - setIsGeneratingSuggestions(false); - } else if (event.type === 'suggestions_error') { - setIsGeneratingSuggestions(false); - } - }); - - return () => { - unsubscribe(); - }; - }, [setSuggestionsCount, setFeatureSuggestions, setIsGeneratingSuggestions]); - // Subscribe to spec regeneration events to clear creating state on completion useEffect(() => { const api = getElectronAPI(); diff --git a/apps/ui/src/components/views/board-view/hooks/use-suggestions-state.ts b/apps/ui/src/components/views/board-view/hooks/use-suggestions-state.ts deleted file mode 100644 index 25e3cd14..00000000 --- a/apps/ui/src/components/views/board-view/hooks/use-suggestions-state.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { FeatureSuggestion } from '@/lib/electron'; - -export function useSuggestionsState() { - const [showSuggestionsDialog, setShowSuggestionsDialog] = useState(false); - const [suggestionsCount, setSuggestionsCount] = useState(0); - const [featureSuggestions, setFeatureSuggestions] = useState([]); - const [isGeneratingSuggestions, setIsGeneratingSuggestions] = useState(false); - - const updateSuggestions = useCallback((suggestions: FeatureSuggestion[]) => { - setFeatureSuggestions(suggestions); - setSuggestionsCount(suggestions.length); - }, []); - - const closeSuggestionsDialog = useCallback(() => { - setShowSuggestionsDialog(false); - }, []); - - return { - // State - showSuggestionsDialog, - suggestionsCount, - featureSuggestions, - isGeneratingSuggestions, - // Setters - setShowSuggestionsDialog, - setSuggestionsCount, - setFeatureSuggestions, - setIsGeneratingSuggestions, - // Helpers - updateSuggestions, - closeSuggestionsDialog, - }; -} diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx index eecadc61..c21711b9 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button'; import { HotkeyButton } from '@/components/ui/hotkey-button'; import { KanbanColumn, KanbanCard } from './components'; import { Feature } from '@/store/app-store'; -import { FastForward, Lightbulb, Archive, Plus, Settings2 } from 'lucide-react'; +import { FastForward, Archive, Plus, Settings2 } from 'lucide-react'; import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; import { useResponsiveKanban } from '@/hooks/use-responsive-kanban'; import { getColumnsWithPipeline, type Column, type ColumnId } from './constants'; @@ -47,8 +47,6 @@ interface KanbanBoardProps { runningAutoTasks: string[]; shortcuts: ReturnType; onStartNextFeatures: () => void; - onShowSuggestions: () => void; - suggestionsCount: number; onArchiveAllVerified: () => void; pipelineConfig: PipelineConfig | null; onOpenPipelineSettings?: () => void; @@ -82,8 +80,6 @@ export function KanbanBoard({ runningAutoTasks, shortcuts, onStartNextFeatures, - onShowSuggestions, - suggestionsCount, onArchiveAllVerified, pipelineConfig, onOpenPipelineSettings, @@ -130,40 +126,20 @@ export function KanbanBoard({ Complete All ) : column.id === 'backlog' ? ( -
- - {columnFeatures.length > 0 && ( - - - Make - - )} -
+ + Make + + ) ) : column.id === 'in_progress' ? ( + + + + + + ); +} + +function GeneratingCard({ job }: { job: GenerationJob }) { + const { removeJob } = useIdeationStore(); + const isError = job.status === 'error'; + + return ( + + +
+
+ {isError ? ( + + ) : ( + + )} +
+

{job.prompt.title}

+

+ {isError ? job.error || 'Failed to generate' : 'Generating ideas...'} +

+
+
+ +
+
+
+ ); +} + +function TagFilter({ + tags, + tagCounts, + selectedTags, + onToggleTag, +}: { + tags: string[]; + tagCounts: Record; + selectedTags: Set; + onToggleTag: (tag: string) => void; +}) { + if (tags.length === 0) return null; + + return ( +
+ {tags.map((tag) => { + const isSelected = selectedTags.has(tag); + const count = tagCounts[tag] || 0; + return ( + + ); + })} + {selectedTags.size > 0 && ( + + )} +
+ ); +} + +export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) { + const currentProject = useAppStore((s) => s.currentProject); + const { generationJobs, removeSuggestionFromJob } = useIdeationStore(); + const [addingId, setAddingId] = useState(null); + const [selectedTags, setSelectedTags] = useState>(new Set()); + + // Separate generating/error jobs from ready jobs with suggestions + const activeJobs = generationJobs.filter( + (j) => j.status === 'generating' || j.status === 'error' + ); + const readyJobs = generationJobs.filter((j) => j.status === 'ready' && j.suggestions.length > 0); + + // Flatten all suggestions with their parent job + const allSuggestions = useMemo( + () => readyJobs.flatMap((job) => job.suggestions.map((suggestion) => ({ suggestion, job }))), + [readyJobs] + ); + + // Extract unique tags and counts from all suggestions + const { availableTags, tagCounts } = useMemo(() => { + const counts: Record = {}; + allSuggestions.forEach(({ job }) => { + const tag = job.prompt.title; + counts[tag] = (counts[tag] || 0) + 1; + }); + return { + availableTags: Object.keys(counts).sort(), + tagCounts: counts, + }; + }, [allSuggestions]); + + // Filter suggestions based on selected tags + const filteredSuggestions = useMemo(() => { + if (selectedTags.size === 0) return allSuggestions; + return allSuggestions.filter(({ job }) => selectedTags.has(job.prompt.title)); + }, [allSuggestions, selectedTags]); + + const generatingCount = generationJobs.filter((j) => j.status === 'generating').length; + + const handleToggleTag = (tag: string) => { + setSelectedTags((prev) => { + const next = new Set(prev); + if (next.has(tag)) { + next.delete(tag); + } else { + next.add(tag); + } + return next; + }); + }; + + const handleAccept = async (suggestion: AnalysisSuggestion, jobId: string) => { + if (!currentProject?.path) { + toast.error('No project selected'); + return; + } + + setAddingId(suggestion.id); + + try { + const api = getElectronAPI(); + const result = await api.ideation?.addSuggestionToBoard(currentProject.path, suggestion); + + if (result?.success) { + toast.success(`Added "${suggestion.title}" to board`); + removeSuggestionFromJob(jobId, suggestion.id); + } else { + toast.error(result?.error || 'Failed to add to board'); + } + } catch (error) { + console.error('Failed to add to board:', error); + toast.error((error as Error).message); + } finally { + setAddingId(null); + } + }; + + const handleRemove = (suggestionId: string, jobId: string) => { + removeSuggestionFromJob(jobId, suggestionId); + toast.info('Idea removed'); + }; + + const isEmpty = allSuggestions.length === 0 && activeJobs.length === 0; + + return ( +
+
+ {/* Status text */} + {(generatingCount > 0 || allSuggestions.length > 0) && ( +

+ {generatingCount > 0 + ? `Generating ${generatingCount} idea${generatingCount > 1 ? 's' : ''}...` + : selectedTags.size > 0 + ? `Showing ${filteredSuggestions.length} of ${allSuggestions.length} ideas` + : `${allSuggestions.length} idea${allSuggestions.length > 1 ? 's' : ''} ready for review`} +

+ )} + + {/* Tag Filters */} + {availableTags.length > 0 && ( + + )} + + {/* Generating/Error Jobs */} + {activeJobs.length > 0 && ( +
+ {activeJobs.map((job) => ( + + ))} +
+ )} + + {/* Suggestions List */} + {filteredSuggestions.length > 0 && ( +
+ {filteredSuggestions.map(({ suggestion, job }) => ( + handleAccept(suggestion, job.id)} + onRemove={() => handleRemove(suggestion.id, job.id)} + isAdding={addingId === suggestion.id} + /> + ))} +
+ )} + + {/* No results after filtering */} + {filteredSuggestions.length === 0 && allSuggestions.length > 0 && ( + + +
+

No ideas match the selected filters

+ +
+
+
+ )} + + {/* Empty State */} + {isEmpty && ( + + +
+ +

No ideas yet

+

+ Generate ideas by selecting a category and prompt type +

+ +
+
+
+ )} +
+
+ ); +} diff --git a/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx b/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx new file mode 100644 index 00000000..abf29c83 --- /dev/null +++ b/apps/ui/src/components/views/ideation-view/components/prompt-category-grid.tsx @@ -0,0 +1,78 @@ +/** + * PromptCategoryGrid - Grid of prompt categories to select from + */ + +import { + ArrowLeft, + Zap, + Palette, + Code, + TrendingUp, + Cpu, + Shield, + Gauge, + Accessibility, + BarChart3, +} from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { PROMPT_CATEGORIES } from '../data/guided-prompts'; +import type { IdeaCategory } from '@automaker/types'; + +interface PromptCategoryGridProps { + onSelect: (category: IdeaCategory) => void; + onBack: () => void; +} + +const iconMap: Record = { + Zap, + Palette, + Code, + TrendingUp, + Cpu, + Shield, + Gauge, + Accessibility, + BarChart3, +}; + +export function PromptCategoryGrid({ onSelect, onBack }: PromptCategoryGridProps) { + return ( +
+
+ {/* Back link */} + + +
+ {PROMPT_CATEGORIES.map((category) => { + const Icon = iconMap[category.icon] || Zap; + return ( + onSelect(category.id)} + > + +
+
+ +
+
+

{category.name}

+

{category.description}

+
+
+
+
+ ); + })} +
+
+
+ ); +} 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 new file mode 100644 index 00000000..b9fd1d31 --- /dev/null +++ b/apps/ui/src/components/views/ideation-view/components/prompt-list.tsx @@ -0,0 +1,162 @@ +/** + * PromptList - List of prompts for a specific category + */ + +import { useState } from 'react'; +import { ArrowLeft, Lightbulb, Loader2, CheckCircle2 } from 'lucide-react'; +import { Card, CardContent } from '@/components/ui/card'; +import { getPromptsByCategory } from '../data/guided-prompts'; +import { useIdeationStore } from '@/store/ideation-store'; +import { useAppStore } from '@/store/app-store'; +import { getElectronAPI } from '@/lib/electron'; +import { toast } from 'sonner'; +import { useNavigate } from '@tanstack/react-router'; +import type { IdeaCategory, IdeationPrompt } from '@automaker/types'; + +interface PromptListProps { + category: IdeaCategory; + onBack: () => void; +} + +export function PromptList({ category, onBack }: PromptListProps) { + const currentProject = useAppStore((s) => s.currentProject); + const { setMode, addGenerationJob, updateJobStatus, generationJobs } = useIdeationStore(); + const [loadingPromptId, setLoadingPromptId] = useState(null); + const [startedPrompts, setStartedPrompts] = useState>(new Set()); + const navigate = useNavigate(); + + const prompts = getPromptsByCategory(category); + + // Check which prompts are already generating + const generatingPromptIds = new Set( + generationJobs.filter((j) => j.status === 'generating').map((j) => j.prompt.id) + ); + + const handleSelectPrompt = async (prompt: IdeationPrompt) => { + if (!currentProject?.path) { + toast.error('No project selected'); + return; + } + + if (loadingPromptId || generatingPromptIds.has(prompt.id)) return; + + setLoadingPromptId(prompt.id); + + // Add a job and navigate to dashboard + const jobId = addGenerationJob(prompt); + setStartedPrompts((prev) => new Set(prev).add(prompt.id)); + + // Show toast and navigate to dashboard + toast.info(`Generating ideas for "${prompt.title}"...`); + setMode('dashboard'); + + try { + const api = getElectronAPI(); + const result = await api.ideation?.generateSuggestions( + currentProject.path, + prompt.id, + category + ); + + if (result?.success && result.suggestions) { + updateJobStatus(jobId, 'ready', result.suggestions); + toast.success(`Generated ${result.suggestions.length} ideas for "${prompt.title}"`, { + duration: 10000, + action: { + label: 'View Ideas', + onClick: () => { + setMode('dashboard'); + navigate({ to: '/ideation' }); + }, + }, + }); + } else { + updateJobStatus( + jobId, + 'error', + undefined, + result?.error || 'Failed to generate suggestions' + ); + toast.error(result?.error || 'Failed to generate suggestions'); + } + } catch (error) { + console.error('Failed to generate suggestions:', error); + updateJobStatus(jobId, 'error', undefined, (error as Error).message); + toast.error((error as Error).message); + } finally { + setLoadingPromptId(null); + } + }; + + return ( +
+
+ {/* Back link */} + + +
+ {prompts.map((prompt) => { + const isLoading = loadingPromptId === prompt.id; + const isGenerating = generatingPromptIds.has(prompt.id); + const isStarted = startedPrompts.has(prompt.id); + const isDisabled = loadingPromptId !== null || isGenerating; + + return ( + !isDisabled && handleSelectPrompt(prompt)} + > + +
+
+ {isLoading || isGenerating ? ( + + ) : isStarted ? ( + + ) : ( + + )} +
+
+

{prompt.title}

+

{prompt.description}

+ {(isLoading || isGenerating) && ( +

Generating in dashboard...

+ )} + {isStarted && !isGenerating && ( +

+ Already generated - check dashboard +

+ )} +
+
+
+
+ ); + })} +
+
+
+ ); +} diff --git a/apps/ui/src/components/views/ideation-view/data/guided-prompts.ts b/apps/ui/src/components/views/ideation-view/data/guided-prompts.ts new file mode 100644 index 00000000..41e8f59c --- /dev/null +++ b/apps/ui/src/components/views/ideation-view/data/guided-prompts.ts @@ -0,0 +1,391 @@ +/** + * Guided prompts for ideation sessions + * Static data that provides pre-made prompts for different categories + */ + +import type { IdeaCategory, IdeationPrompt, PromptCategory } from '@automaker/types'; + +export const PROMPT_CATEGORIES: PromptCategory[] = [ + { + id: 'feature', + name: 'Features', + icon: 'Zap', + description: 'New capabilities and functionality', + }, + { + id: 'ux-ui', + name: 'UX/UI', + icon: 'Palette', + description: 'Design and user experience improvements', + }, + { + id: 'dx', + name: 'Developer Experience', + icon: 'Code', + description: 'Developer tooling and workflows', + }, + { + id: 'growth', + name: 'Growth', + icon: 'TrendingUp', + description: 'User engagement and retention', + }, + { + id: 'technical', + name: 'Technical', + icon: 'Cpu', + description: 'Architecture and infrastructure', + }, + { + id: 'security', + name: 'Security', + icon: 'Shield', + description: 'Security and privacy improvements', + }, + { + id: 'performance', + name: 'Performance', + icon: 'Gauge', + description: 'Speed and optimization', + }, + { + id: 'accessibility', + name: 'Accessibility', + icon: 'Accessibility', + description: 'Inclusive design for all users', + }, + { + id: 'analytics', + name: 'Analytics', + icon: 'BarChart3', + description: 'Data insights and tracking', + }, +]; + +export const GUIDED_PROMPTS: IdeationPrompt[] = [ + // Feature prompts + { + id: 'feature-missing', + category: 'feature', + title: 'Missing Features', + description: 'Discover features users might expect', + prompt: + "Analyze this codebase and identify features that users of similar applications typically expect but are missing here. Consider the app's domain, target users, and common patterns in similar products.", + }, + { + id: 'feature-automation', + category: 'feature', + title: 'Automation Opportunities', + description: 'Find manual processes that could be automated', + prompt: + 'Review this codebase and identify manual processes or repetitive tasks that could be automated. Look for patterns where users might be doing things repeatedly that software could handle.', + }, + { + id: 'feature-integrations', + category: 'feature', + title: 'Integration Ideas', + description: 'Identify valuable third-party integrations', + prompt: + "Based on this codebase, what third-party services or APIs would provide value if integrated? Consider the app's domain and what complementary services users might need.", + }, + { + id: 'feature-workflow', + category: 'feature', + title: 'Workflow Improvements', + description: 'Streamline user workflows', + prompt: + 'Analyze the user workflows in this application. What steps could be combined, eliminated, or automated? Where are users likely spending too much time on repetitive tasks?', + }, + + // UX/UI prompts + { + id: 'ux-friction', + category: 'ux-ui', + title: 'Friction Points', + description: 'Identify where users might get stuck', + prompt: + 'Analyze the user flows in this codebase and identify potential friction points. Where might users get confused, stuck, or frustrated? Look at form submissions, navigation, error states, and complex interactions.', + }, + { + id: 'ux-empty-states', + category: 'ux-ui', + title: 'Empty States', + description: 'Improve empty state experiences', + prompt: + "Review the components in this codebase and identify empty states that could be improved. How can we guide users when there's no content? Consider onboarding, helpful prompts, and sample data.", + }, + { + id: 'ux-accessibility', + category: 'ux-ui', + title: 'Accessibility Improvements', + description: 'Enhance accessibility and inclusivity', + prompt: + 'Analyze this codebase for accessibility improvements. Consider keyboard navigation, screen reader support, color contrast, focus states, and ARIA labels. What specific improvements would make this more accessible?', + }, + { + id: 'ux-mobile', + category: 'ux-ui', + title: 'Mobile Experience', + description: 'Optimize for mobile users', + prompt: + 'Review this codebase from a mobile-first perspective. What improvements would enhance the mobile user experience? Consider touch targets, responsive layouts, and mobile-specific interactions.', + }, + { + id: 'ux-feedback', + category: 'ux-ui', + title: 'User Feedback', + description: 'Improve feedback and status indicators', + prompt: + 'Analyze how this application communicates with users. Where are loading states, success messages, or error handling missing or unclear? What feedback would help users understand what is happening?', + }, + + // DX prompts + { + id: 'dx-documentation', + category: 'dx', + title: 'Documentation Gaps', + description: 'Identify missing documentation', + prompt: + 'Review this codebase and identify areas lacking documentation. What would help new developers understand the architecture, APIs, and conventions? Consider inline comments, READMEs, and API docs.', + }, + { + id: 'dx-testing', + category: 'dx', + title: 'Testing Improvements', + description: 'Enhance test coverage and quality', + prompt: + 'Analyze the testing patterns in this codebase. What areas need better test coverage? What types of tests are missing? Consider unit tests, integration tests, and end-to-end tests.', + }, + { + id: 'dx-tooling', + category: 'dx', + title: 'Developer Tooling', + description: 'Improve development workflows', + prompt: + 'Review the development setup and tooling in this codebase. What improvements would speed up development? Consider build times, hot reload, debugging tools, and developer scripts.', + }, + { + id: 'dx-error-handling', + category: 'dx', + title: 'Error Handling', + description: 'Improve error messages and debugging', + prompt: + 'Analyze error handling in this codebase. Where are error messages unclear or missing? What would help developers debug issues faster? Consider logging, error boundaries, and stack traces.', + }, + + // Growth prompts + { + id: 'growth-onboarding', + category: 'growth', + title: 'Onboarding Flow', + description: 'Improve new user experience', + prompt: + "Analyze this application's onboarding experience. How can we help new users understand the value and get started quickly? Consider tutorials, progressive disclosure, and quick wins.", + }, + { + id: 'growth-engagement', + category: 'growth', + title: 'User Engagement', + description: 'Increase user retention and activity', + prompt: + 'Review this application and suggest features that would increase user engagement and retention. What would bring users back daily? Consider notifications, streaks, social features, and personalization.', + }, + { + id: 'growth-sharing', + category: 'growth', + title: 'Shareability', + description: 'Make the app more shareable', + prompt: + 'How can this application be made more shareable? What features would encourage users to invite others or share their work? Consider collaboration, public profiles, and export features.', + }, + { + id: 'growth-monetization', + category: 'growth', + title: 'Monetization Ideas', + description: 'Identify potential revenue streams', + prompt: + 'Based on this codebase, what features or tiers could support monetization? Consider premium features, usage limits, team features, and integrations that users would pay for.', + }, + + // Technical prompts + { + id: 'tech-performance', + category: 'technical', + title: 'Performance Optimization', + description: 'Identify performance bottlenecks', + prompt: + 'Analyze this codebase for performance optimization opportunities. Where are the likely bottlenecks? Consider database queries, API calls, bundle size, rendering, and caching strategies.', + }, + { + id: 'tech-architecture', + category: 'technical', + title: 'Architecture Review', + description: 'Evaluate and improve architecture', + prompt: + 'Review the architecture of this codebase. What improvements would make it more maintainable, scalable, or testable? Consider separation of concerns, dependency management, and patterns.', + }, + { + id: 'tech-debt', + category: 'technical', + title: 'Technical Debt', + description: 'Identify areas needing refactoring', + prompt: + 'Identify technical debt in this codebase. What areas are becoming hard to maintain or understand? What refactoring would have the highest impact? Consider duplicated code, complexity, and outdated patterns.', + }, + { + id: 'tech-security', + category: 'technical', + title: 'Security Review', + description: 'Identify security improvements', + prompt: + 'Review this codebase for security improvements. What best practices are missing? Consider authentication, authorization, input validation, and data protection. Note: This is for improvement suggestions, not a security audit.', + }, + + // Security prompts + { + id: 'security-auth', + category: 'security', + title: 'Authentication Security', + description: 'Review authentication mechanisms', + prompt: + 'Analyze the authentication system in this codebase. What security improvements would strengthen user authentication? Consider password policies, session management, MFA, and token handling.', + }, + { + id: 'security-data', + category: 'security', + title: 'Data Protection', + description: 'Protect sensitive user data', + prompt: + 'Review how this application handles sensitive data. What improvements would better protect user privacy? Consider encryption, data minimization, secure storage, and data retention policies.', + }, + { + id: 'security-input', + category: 'security', + title: 'Input Validation', + description: 'Prevent injection attacks', + prompt: + 'Analyze input handling in this codebase. Where could input validation be strengthened? Consider SQL injection, XSS, command injection, and file upload vulnerabilities.', + }, + { + id: 'security-api', + category: 'security', + title: 'API Security', + description: 'Secure API endpoints', + prompt: + 'Review the API security in this codebase. What improvements would make the API more secure? Consider rate limiting, authorization, CORS, and request validation.', + }, + + // Performance prompts + { + id: 'perf-frontend', + category: 'performance', + title: 'Frontend Performance', + description: 'Optimize UI rendering and loading', + prompt: + 'Analyze the frontend performance of this application. What optimizations would improve load times and responsiveness? Consider bundle splitting, lazy loading, memoization, and render optimization.', + }, + { + id: 'perf-backend', + category: 'performance', + title: 'Backend Performance', + description: 'Optimize server-side operations', + prompt: + 'Review backend performance in this codebase. What optimizations would improve response times? Consider database queries, caching strategies, async operations, and resource pooling.', + }, + { + id: 'perf-database', + category: 'performance', + title: 'Database Optimization', + description: 'Improve query performance', + prompt: + 'Analyze database interactions in this codebase. What optimizations would improve data access performance? Consider indexing, query optimization, denormalization, and connection pooling.', + }, + { + id: 'perf-caching', + category: 'performance', + title: 'Caching Strategies', + description: 'Implement effective caching', + prompt: + 'Review caching opportunities in this application. Where would caching provide the most benefit? Consider API responses, computed values, static assets, and session data.', + }, + + // Accessibility prompts + { + id: 'a11y-keyboard', + category: 'accessibility', + title: 'Keyboard Navigation', + description: 'Enable full keyboard access', + prompt: + 'Analyze keyboard accessibility in this codebase. What improvements would enable users to navigate entirely with keyboard? Consider focus management, tab order, and keyboard shortcuts.', + }, + { + id: 'a11y-screen-reader', + category: 'accessibility', + title: 'Screen Reader Support', + description: 'Improve screen reader experience', + prompt: + 'Review screen reader compatibility in this application. What improvements would help users with visual impairments? Consider ARIA labels, semantic HTML, live regions, and alt text.', + }, + { + id: 'a11y-visual', + category: 'accessibility', + title: 'Visual Accessibility', + description: 'Improve visual design for all users', + prompt: + 'Analyze visual accessibility in this codebase. What improvements would help users with visual impairments? Consider color contrast, text sizing, focus indicators, and reduced motion.', + }, + { + id: 'a11y-forms', + category: 'accessibility', + title: 'Accessible Forms', + description: 'Make forms usable for everyone', + prompt: + 'Review form accessibility in this application. What improvements would make forms more accessible? Consider labels, error messages, required field indicators, and input assistance.', + }, + + // Analytics prompts + { + id: 'analytics-tracking', + category: 'analytics', + title: 'User Tracking', + description: 'Track key user behaviors', + prompt: + 'Analyze this application for analytics opportunities. What user behaviors should be tracked to understand engagement? Consider page views, feature usage, conversion funnels, and session duration.', + }, + { + id: 'analytics-metrics', + category: 'analytics', + title: 'Key Metrics', + description: 'Define success metrics', + prompt: + 'Based on this codebase, what key metrics should be tracked? Consider user acquisition, retention, engagement, and feature adoption. What dashboards would be most valuable?', + }, + { + id: 'analytics-errors', + category: 'analytics', + title: 'Error Monitoring', + description: 'Track and analyze errors', + prompt: + 'Review error handling in this codebase for monitoring opportunities. What error tracking would help identify and fix issues faster? Consider error aggregation, alerting, and stack traces.', + }, + { + id: 'analytics-performance', + category: 'analytics', + title: 'Performance Monitoring', + description: 'Track application performance', + prompt: + 'Analyze this application for performance monitoring opportunities. What metrics would help identify bottlenecks? Consider load times, API response times, and resource usage.', + }, +]; + +export function getPromptsByCategory(category: IdeaCategory): IdeationPrompt[] { + return GUIDED_PROMPTS.filter((p) => p.category === category); +} + +export function getPromptById(id: string): IdeationPrompt | undefined { + return GUIDED_PROMPTS.find((p) => p.id === id); +} + +export function getCategoryById(id: IdeaCategory): PromptCategory | undefined { + return PROMPT_CATEGORIES.find((c) => c.id === id); +} diff --git a/apps/ui/src/components/views/ideation-view/index.tsx b/apps/ui/src/components/views/ideation-view/index.tsx new file mode 100644 index 00000000..aa962ae8 --- /dev/null +++ b/apps/ui/src/components/views/ideation-view/index.tsx @@ -0,0 +1,208 @@ +/** + * IdeationView - Main view for brainstorming and idea management + * Dashboard-first design with Generate Ideas flow + */ + +import { useCallback } from 'react'; +import { useIdeationStore } from '@/store/ideation-store'; +import { useAppStore } from '@/store/app-store'; +import { PromptCategoryGrid } from './components/prompt-category-grid'; +import { PromptList } from './components/prompt-list'; +import { IdeationDashboard } from './components/ideation-dashboard'; +import { getCategoryById } from './data/guided-prompts'; +import { Button } from '@/components/ui/button'; +import { ArrowLeft, ChevronRight, Lightbulb } from 'lucide-react'; +import type { IdeaCategory } from '@automaker/types'; +import type { IdeationMode } from '@/store/ideation-store'; + +// Get subtitle text based on current mode +function getSubtitle(currentMode: IdeationMode, selectedCategory: IdeaCategory | null): string { + if (currentMode === 'dashboard') { + return 'Review and accept generated ideas'; + } + if (currentMode === 'prompts') { + if (selectedCategory) { + const categoryInfo = getCategoryById(selectedCategory); + return `Select a prompt from ${categoryInfo?.name || 'category'}`; + } + return 'Select a category to generate ideas'; + } + return ''; +} + +// Breadcrumb component - compact inline breadcrumbs +function IdeationBreadcrumbs({ + currentMode, + selectedCategory, + onNavigate, +}: { + currentMode: IdeationMode; + selectedCategory: IdeaCategory | null; + onNavigate: (mode: IdeationMode, category?: IdeaCategory | null) => void; +}) { + const categoryInfo = selectedCategory ? getCategoryById(selectedCategory) : null; + + // On dashboard, no breadcrumbs needed (it's the root) + if (currentMode === 'dashboard') { + return null; + } + + return ( + + ); +} + +// Header shown on all pages - matches other view headers +function IdeationHeader({ + currentMode, + selectedCategory, + onNavigate, + onGenerateIdeas, + onBack, +}: { + currentMode: IdeationMode; + selectedCategory: IdeaCategory | null; + onNavigate: (mode: IdeationMode, category?: IdeaCategory | null) => void; + onGenerateIdeas: () => void; + onBack: () => void; +}) { + const subtitle = getSubtitle(currentMode, selectedCategory); + const showBackButton = currentMode === 'prompts'; + + return ( +
+
+ {showBackButton && ( + + )} +
+
+ +

Ideation

+
+ {currentMode === 'dashboard' ? ( +

{subtitle}

+ ) : ( + + )} +
+
+ +
+ +
+
+ ); +} + +export function IdeationView() { + const currentProject = useAppStore((s) => s.currentProject); + const { currentMode, selectedCategory, setMode, setCategory } = useIdeationStore(); + + const handleNavigate = useCallback( + (mode: IdeationMode, category?: IdeaCategory | null) => { + setMode(mode); + if (category !== undefined) { + setCategory(category); + } else if (mode !== 'prompts') { + setCategory(null); + } + }, + [setMode, setCategory] + ); + + const handleSelectCategory = useCallback( + (category: IdeaCategory) => { + setCategory(category); + }, + [setCategory] + ); + + const handleBackFromPrompts = useCallback(() => { + // If viewing a category, go back to category grid + if (selectedCategory) { + setCategory(null); + return; + } + // Otherwise, go back to dashboard + setMode('dashboard'); + }, [selectedCategory, setCategory, setMode]); + + const handleGenerateIdeas = useCallback(() => { + setMode('prompts'); + setCategory(null); + }, [setMode, setCategory]); + + if (!currentProject) { + return ( +
+
+

Open a project to start brainstorming ideas

+
+
+ ); + } + + return ( +
+ {/* Header with breadcrumbs - always shown */} + + + {/* Dashboard - main view */} + {currentMode === 'dashboard' && } + + {/* Prompts - category selection */} + {currentMode === 'prompts' && !selectedCategory && ( + + )} + + {/* Prompts - prompt selection within category */} + {currentMode === 'prompts' && selectedCategory && ( + + )} +
+ ); +} diff --git a/apps/ui/src/hooks/use-keyboard-shortcuts.ts b/apps/ui/src/hooks/use-keyboard-shortcuts.ts index 4f5a0234..ae3c130a 100644 --- a/apps/ui/src/hooks/use-keyboard-shortcuts.ts +++ b/apps/ui/src/hooks/use-keyboard-shortcuts.ts @@ -1,5 +1,5 @@ -import { useEffect, useCallback } from 'react'; -import { useAppStore, parseShortcut } from '@/store/app-store'; +import { useEffect, useCallback, useMemo } from 'react'; +import { useAppStore, parseShortcut, DEFAULT_KEYBOARD_SHORTCUTS } from '@/store/app-store'; export interface KeyboardShortcut { key: string; // Can be simple "K" or with modifiers "Shift+N", "Cmd+K" @@ -237,8 +237,18 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { /** * Hook to get current keyboard shortcuts from store * This replaces the static constants and allows customization + * Merges with defaults to ensure new shortcuts are always available */ export function useKeyboardShortcutsConfig() { const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts); - return keyboardShortcuts; + + // Merge with defaults to ensure new shortcuts are available + // even if user's persisted state predates them + return useMemo( + () => ({ + ...DEFAULT_KEYBOARD_SHORTCUTS, + ...keyboardShortcuts, + }), + [keyboardShortcuts] + ); } diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 58125806..ef9c6bb9 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -13,6 +13,16 @@ import type { AgentModel, GitHubComment, IssueCommentsResult, + Idea, + IdeaCategory, + IdeationSession, + IdeationMessage, + ProjectAnalysisResult, + AnalysisSuggestion, + StartSessionOptions, + CreateIdeaInput, + UpdateIdeaInput, + ConvertToFeatureOptions, } from '@automaker/types'; import { getJSON, setJSON, removeItem } from './storage'; @@ -30,6 +40,94 @@ export type { IssueCommentsResult, }; +// Re-export ideation types +export type { + Idea, + IdeaCategory, + IdeationSession, + IdeationMessage, + ProjectAnalysisResult, + AnalysisSuggestion, + StartSessionOptions, + CreateIdeaInput, + UpdateIdeaInput, + ConvertToFeatureOptions, +}; + +// Ideation API interface +export interface IdeationAPI { + // Session management + startSession: ( + projectPath: string, + options?: StartSessionOptions + ) => Promise<{ success: boolean; session?: IdeationSession; error?: string }>; + getSession: ( + projectPath: string, + sessionId: string + ) => Promise<{ + success: boolean; + session?: IdeationSession; + messages?: IdeationMessage[]; + error?: string; + }>; + sendMessage: ( + sessionId: string, + message: string, + options?: { imagePaths?: string[]; model?: string } + ) => Promise<{ success: boolean; error?: string }>; + stopSession: (sessionId: string) => Promise<{ success: boolean; error?: string }>; + + // Ideas CRUD + listIdeas: (projectPath: string) => Promise<{ success: boolean; ideas?: Idea[]; error?: string }>; + createIdea: ( + projectPath: string, + idea: CreateIdeaInput + ) => Promise<{ success: boolean; idea?: Idea; error?: string }>; + getIdea: ( + projectPath: string, + ideaId: string + ) => Promise<{ success: boolean; idea?: Idea; error?: string }>; + updateIdea: ( + projectPath: string, + ideaId: string, + updates: UpdateIdeaInput + ) => Promise<{ success: boolean; idea?: Idea; error?: string }>; + deleteIdea: ( + projectPath: string, + ideaId: string + ) => Promise<{ success: boolean; error?: string }>; + + // Project analysis + analyzeProject: ( + projectPath: string + ) => Promise<{ success: boolean; analysis?: ProjectAnalysisResult; error?: string }>; + + // Generate suggestions from a prompt + generateSuggestions: ( + projectPath: string, + promptId: string, + category: IdeaCategory, + count?: number + ) => Promise<{ success: boolean; suggestions?: AnalysisSuggestion[]; error?: string }>; + + // Convert to feature + convertToFeature: ( + projectPath: string, + ideaId: string, + options?: ConvertToFeatureOptions + ) => Promise<{ success: boolean; feature?: any; featureId?: string; error?: string }>; + + // Add suggestion directly to board as feature + addSuggestionToBoard: ( + projectPath: string, + suggestion: AnalysisSuggestion + ) => Promise<{ success: boolean; featureId?: string; error?: string }>; + + // Event subscriptions + onStream: (callback: (event: any) => void) => () => void; + onAnalysisEvent: (callback: (event: any) => void) => () => void; +} + export interface FileEntry { name: string; isDirectory: boolean; @@ -647,6 +745,7 @@ export interface ElectronAPI { error?: string; }>; }; + ideation?: IdeationAPI; } // Note: Window interface is declared in @/types/electron.d.ts diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 32bd88f8..087470ef 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -26,6 +26,13 @@ import type { GitHubPR, IssueValidationInput, IssueValidationEvent, + IdeationAPI, + IdeaCategory, + AnalysisSuggestion, + StartSessionOptions, + CreateIdeaInput, + UpdateIdeaInput, + ConvertToFeatureOptions, } from './electron'; import type { Message, SessionListItem } from '@/types/electron'; import type { Feature, ClaudeUsageResponse } from '@/store/app-store'; @@ -368,7 +375,9 @@ type EventType = | 'suggestions:event' | 'spec-regeneration:event' | 'issue-validation:event' - | 'backlog-plan:event'; + | 'backlog-plan:event' + | 'ideation:stream' + | 'ideation:analysis'; type EventCallback = (payload: unknown) => void; @@ -1640,6 +1649,88 @@ export class HttpApiClient implements ElectronAPI { }, }; + // Ideation API - brainstorming and idea management + ideation: IdeationAPI = { + startSession: (projectPath: string, options?: StartSessionOptions) => + this.post('/api/ideation/session/start', { projectPath, options }), + + getSession: (projectPath: string, sessionId: string) => + this.post('/api/ideation/session/get', { projectPath, sessionId }), + + sendMessage: ( + sessionId: string, + message: string, + options?: { imagePaths?: string[]; model?: string } + ) => this.post('/api/ideation/session/message', { sessionId, message, options }), + + stopSession: (sessionId: string) => this.post('/api/ideation/session/stop', { sessionId }), + + listIdeas: (projectPath: string) => this.post('/api/ideation/ideas/list', { projectPath }), + + createIdea: (projectPath: string, idea: CreateIdeaInput) => + this.post('/api/ideation/ideas/create', { projectPath, idea }), + + getIdea: (projectPath: string, ideaId: string) => + this.post('/api/ideation/ideas/get', { projectPath, ideaId }), + + updateIdea: (projectPath: string, ideaId: string, updates: UpdateIdeaInput) => + this.post('/api/ideation/ideas/update', { projectPath, ideaId, updates }), + + deleteIdea: (projectPath: string, ideaId: string) => + this.post('/api/ideation/ideas/delete', { projectPath, ideaId }), + + analyzeProject: (projectPath: string) => this.post('/api/ideation/analyze', { projectPath }), + + generateSuggestions: ( + projectPath: string, + promptId: string, + category: IdeaCategory, + count?: number + ) => + this.post('/api/ideation/suggestions/generate', { projectPath, promptId, category, count }), + + convertToFeature: (projectPath: string, ideaId: string, options?: ConvertToFeatureOptions) => + this.post('/api/ideation/convert', { projectPath, ideaId, ...options }), + + addSuggestionToBoard: async (projectPath: string, suggestion: AnalysisSuggestion) => { + // Create a feature directly from the suggestion + const result = await this.post<{ success: boolean; feature?: Feature; error?: string }>( + '/api/features/create', + { + projectPath, + feature: { + title: suggestion.title, + description: + suggestion.description + + (suggestion.rationale ? `\n\n**Rationale:** ${suggestion.rationale}` : ''), + category: + suggestion.category === 'ux-ui' + ? 'enhancement' + : suggestion.category === 'dx' + ? 'chore' + : suggestion.category === 'technical' + ? 'refactor' + : 'feature', + status: 'backlog', + }, + } + ); + return { + success: result.success, + featureId: result.feature?.id, + error: result.error, + }; + }, + + onStream: (callback: (event: any) => void): (() => void) => { + return this.subscribeToEvent('ideation:stream', callback as EventCallback); + }, + + onAnalysisEvent: (callback: (event: any) => void): (() => void) => { + return this.subscribeToEvent('ideation:analysis', callback as EventCallback); + }, + }; + // MCP API - Test MCP server connections and list tools // SECURITY: Only accepts serverId, not arbitrary serverConfig, to prevent // drive-by command execution attacks. Servers must be saved first. diff --git a/apps/ui/src/routes/ideation.tsx b/apps/ui/src/routes/ideation.tsx new file mode 100644 index 00000000..6352d6aa --- /dev/null +++ b/apps/ui/src/routes/ideation.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { IdeationView } from '@/components/views/ideation-view'; + +export const Route = createFileRoute('/ideation')({ + component: IdeationView, +}); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index a57e4d93..ace36cf6 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -29,7 +29,8 @@ export type ViewMode = | 'profiles' | 'running-agents' | 'terminal' - | 'wiki'; + | 'wiki' + | 'ideation'; export type ThemeMode = | 'light' @@ -154,6 +155,9 @@ export interface KeyboardShortcuts { settings: string; profiles: string; terminal: string; + ideation: string; + githubIssues: string; + githubPrs: string; // UI shortcuts toggleSidebar: string; @@ -186,6 +190,9 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { settings: 'S', profiles: 'M', terminal: 'T', + ideation: 'I', + githubIssues: 'G', + githubPrs: 'R', // UI toggleSidebar: '`', diff --git a/apps/ui/src/store/ideation-store.ts b/apps/ui/src/store/ideation-store.ts new file mode 100644 index 00000000..cfc564ff --- /dev/null +++ b/apps/ui/src/store/ideation-store.ts @@ -0,0 +1,324 @@ +/** + * Ideation Store - State management for brainstorming and idea management + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { + Idea, + IdeaCategory, + IdeaStatus, + IdeationPrompt, + AnalysisSuggestion, + ProjectAnalysisResult, +} from '@automaker/types'; + +// ============================================================================ +// Generation Job Types +// ============================================================================ + +export type GenerationJobStatus = 'generating' | 'ready' | 'error'; + +export interface GenerationJob { + id: string; + prompt: IdeationPrompt; + status: GenerationJobStatus; + suggestions: AnalysisSuggestion[]; + error: string | null; + startedAt: string; + completedAt: string | null; +} + +// ============================================================================ +// State Interface +// ============================================================================ + +export type IdeationMode = 'dashboard' | 'prompts'; + +interface IdeationState { + // Ideas (saved for later) + ideas: Idea[]; + selectedIdeaId: string | null; + + // Generation jobs (multiple concurrent generations) + generationJobs: GenerationJob[]; + selectedJobId: string | null; + + // Legacy - keep for backwards compat during transition + suggestions: AnalysisSuggestion[]; + selectedPrompt: IdeationPrompt | null; + isGenerating: boolean; + generatingError: string | null; + + // Analysis + analysisResult: ProjectAnalysisResult | null; + isAnalyzing: boolean; + analysisProgress: number; + analysisMessage: string; + + // UI state + currentMode: IdeationMode; + selectedCategory: IdeaCategory | null; + filterStatus: IdeaStatus | 'all'; +} + +// ============================================================================ +// Actions Interface +// ============================================================================ + +interface IdeationActions { + // Ideas + setIdeas: (ideas: Idea[]) => void; + addIdea: (idea: Idea) => void; + updateIdea: (id: string, updates: Partial) => void; + removeIdea: (id: string) => void; + setSelectedIdea: (id: string | null) => void; + getSelectedIdea: () => Idea | null; + + // Generation Jobs + addGenerationJob: (prompt: IdeationPrompt) => string; + updateJobStatus: ( + jobId: string, + status: GenerationJobStatus, + suggestions?: AnalysisSuggestion[], + error?: string + ) => void; + removeJob: (jobId: string) => void; + clearCompletedJobs: () => void; + setSelectedJob: (jobId: string | null) => void; + getJob: (jobId: string) => GenerationJob | null; + removeSuggestionFromJob: (jobId: string, suggestionId: string) => void; + appendSuggestionsToJob: (jobId: string, suggestions: AnalysisSuggestion[]) => void; + setJobGenerating: (jobId: string, generating: boolean) => void; + + // Legacy Suggestions (kept for backwards compat) + setSuggestions: (suggestions: AnalysisSuggestion[]) => void; + clearSuggestions: () => void; + removeSuggestion: (id: string) => void; + setSelectedPrompt: (prompt: IdeationPrompt | null) => void; + setIsGenerating: (isGenerating: boolean) => void; + setGeneratingError: (error: string | null) => void; + + // Analysis + setAnalysisResult: (result: ProjectAnalysisResult | null) => void; + setIsAnalyzing: (isAnalyzing: boolean) => void; + setAnalysisProgress: (progress: number, message?: string) => void; + + // UI + setMode: (mode: IdeationMode) => void; + setCategory: (category: IdeaCategory | null) => void; + setFilterStatus: (status: IdeaStatus | 'all') => void; + + // Reset + reset: () => void; + resetSuggestions: () => void; +} + +// ============================================================================ +// Initial State +// ============================================================================ + +const initialState: IdeationState = { + ideas: [], + selectedIdeaId: null, + generationJobs: [], + selectedJobId: null, + suggestions: [], + selectedPrompt: null, + isGenerating: false, + generatingError: null, + analysisResult: null, + isAnalyzing: false, + analysisProgress: 0, + analysisMessage: '', + currentMode: 'dashboard', + selectedCategory: null, + filterStatus: 'all', +}; + +// ============================================================================ +// Store +// ============================================================================ + +export const useIdeationStore = create()( + persist( + (set, get) => ({ + ...initialState, + + // Ideas + setIdeas: (ideas) => set({ ideas }), + + addIdea: (idea) => + set((state) => ({ + ideas: [idea, ...state.ideas], + })), + + updateIdea: (id, updates) => + set((state) => ({ + ideas: state.ideas.map((idea) => (idea.id === id ? { ...idea, ...updates } : idea)), + })), + + removeIdea: (id) => + set((state) => ({ + ideas: state.ideas.filter((idea) => idea.id !== id), + selectedIdeaId: state.selectedIdeaId === id ? null : state.selectedIdeaId, + })), + + setSelectedIdea: (id) => set({ selectedIdeaId: id }), + + getSelectedIdea: () => { + const state = get(); + return state.ideas.find((idea) => idea.id === state.selectedIdeaId) || null; + }, + + // Generation Jobs + addGenerationJob: (prompt) => { + const jobId = `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + const job: GenerationJob = { + id: jobId, + prompt, + status: 'generating', + suggestions: [], + error: null, + startedAt: new Date().toISOString(), + completedAt: null, + }; + set((state) => ({ + generationJobs: [job, ...state.generationJobs], + })); + return jobId; + }, + + updateJobStatus: (jobId, status, suggestions, error) => + set((state) => ({ + generationJobs: state.generationJobs.map((job) => + job.id === jobId + ? { + ...job, + status, + suggestions: suggestions || job.suggestions, + error: error || null, + completedAt: status !== 'generating' ? new Date().toISOString() : null, + } + : job + ), + })), + + removeJob: (jobId) => + set((state) => ({ + generationJobs: state.generationJobs.filter((job) => job.id !== jobId), + selectedJobId: state.selectedJobId === jobId ? null : state.selectedJobId, + })), + + clearCompletedJobs: () => + set((state) => ({ + generationJobs: state.generationJobs.filter((job) => job.status === 'generating'), + })), + + setSelectedJob: (jobId) => set({ selectedJobId: jobId }), + + getJob: (jobId) => { + const state = get(); + return state.generationJobs.find((job) => job.id === jobId) || null; + }, + + removeSuggestionFromJob: (jobId, suggestionId) => + set((state) => ({ + generationJobs: state.generationJobs.map((job) => + job.id === jobId + ? { + ...job, + suggestions: job.suggestions.filter((s) => s.id !== suggestionId), + } + : job + ), + })), + + appendSuggestionsToJob: (jobId, suggestions) => + set((state) => ({ + generationJobs: state.generationJobs.map((job) => + job.id === jobId + ? { + ...job, + suggestions: [...job.suggestions, ...suggestions], + status: 'ready' as const, + } + : job + ), + })), + + setJobGenerating: (jobId, generating) => + set((state) => ({ + generationJobs: state.generationJobs.map((job) => + job.id === jobId + ? { + ...job, + status: generating ? ('generating' as const) : ('ready' as const), + } + : job + ), + })), + + // Suggestions (legacy) + setSuggestions: (suggestions) => set({ suggestions }), + + clearSuggestions: () => set({ suggestions: [], generatingError: null }), + + removeSuggestion: (id) => + set((state) => ({ + suggestions: state.suggestions.filter((s) => s.id !== id), + })), + + setSelectedPrompt: (prompt) => set({ selectedPrompt: prompt }), + + setIsGenerating: (isGenerating) => set({ isGenerating }), + + setGeneratingError: (error) => set({ generatingError: error }), + + // Analysis + setAnalysisResult: (result) => set({ analysisResult: result }), + + setIsAnalyzing: (isAnalyzing) => + set({ + isAnalyzing, + analysisProgress: isAnalyzing ? 0 : get().analysisProgress, + analysisMessage: isAnalyzing ? 'Starting analysis...' : '', + }), + + setAnalysisProgress: (progress, message) => + set({ + analysisProgress: progress, + analysisMessage: message || get().analysisMessage, + }), + + // UI + setMode: (mode) => set({ currentMode: mode }), + + setCategory: (category) => set({ selectedCategory: category }), + + setFilterStatus: (status) => set({ filterStatus: status }), + + // Reset + reset: () => set(initialState), + + resetSuggestions: () => + set({ + suggestions: [], + selectedPrompt: null, + isGenerating: false, + generatingError: null, + }), + }), + { + name: 'automaker-ideation-store', + version: 3, + partialize: (state) => ({ + // Only persist these fields + ideas: state.ideas, + generationJobs: state.generationJobs, + analysisResult: state.analysisResult, + filterStatus: state.filterStatus, + }), + } + ) +); diff --git a/libs/platform/src/index.ts b/libs/platform/src/index.ts index 81ffe224..38ddda1b 100644 --- a/libs/platform/src/index.ts +++ b/libs/platform/src/index.ts @@ -23,6 +23,17 @@ export { getCredentialsPath, getProjectSettingsPath, ensureDataDir, + // Ideation paths + getIdeationDir, + getIdeasDir, + getIdeaDir, + getIdeaPath, + getIdeaAttachmentsDir, + getIdeationSessionsDir, + getIdeationSessionPath, + getIdeationDraftsDir, + getIdeationAnalysisPath, + ensureIdeationDir, } from './paths.js'; // Subprocess management diff --git a/libs/platform/src/paths.ts b/libs/platform/src/paths.ts index 6fea2200..612f155c 100644 --- a/libs/platform/src/paths.ts +++ b/libs/platform/src/paths.ts @@ -188,6 +188,140 @@ export async function ensureAutomakerDir(projectPath: string): Promise { return automakerDir; } +// ============================================================================ +// Ideation Paths +// ============================================================================ + +/** + * Get the ideation directory for a project + * + * Contains ideas, sessions, and drafts for brainstorming. + * + * @param projectPath - Absolute path to project directory + * @returns Absolute path to {projectPath}/.automaker/ideation + */ +export function getIdeationDir(projectPath: string): string { + return path.join(getAutomakerDir(projectPath), 'ideation'); +} + +/** + * Get the ideas directory for a project + * + * Contains subdirectories for each idea, keyed by ideaId. + * + * @param projectPath - Absolute path to project directory + * @returns Absolute path to {projectPath}/.automaker/ideation/ideas + */ +export function getIdeasDir(projectPath: string): string { + return path.join(getIdeationDir(projectPath), 'ideas'); +} + +/** + * Get the directory for a specific idea + * + * Contains idea metadata and attachments. + * + * @param projectPath - Absolute path to project directory + * @param ideaId - Idea identifier + * @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId} + */ +export function getIdeaDir(projectPath: string, ideaId: string): string { + return path.join(getIdeasDir(projectPath), ideaId); +} + +/** + * Get the idea metadata file path + * + * Stores the idea JSON data. + * + * @param projectPath - Absolute path to project directory + * @param ideaId - Idea identifier + * @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId}/idea.json + */ +export function getIdeaPath(projectPath: string, ideaId: string): string { + return path.join(getIdeaDir(projectPath, ideaId), 'idea.json'); +} + +/** + * Get the idea attachments directory + * + * Stores images and other attachments for an idea. + * + * @param projectPath - Absolute path to project directory + * @param ideaId - Idea identifier + * @returns Absolute path to {projectPath}/.automaker/ideation/ideas/{ideaId}/attachments + */ +export function getIdeaAttachmentsDir(projectPath: string, ideaId: string): string { + return path.join(getIdeaDir(projectPath, ideaId), 'attachments'); +} + +/** + * Get the ideation sessions directory for a project + * + * Contains conversation history for ideation sessions. + * + * @param projectPath - Absolute path to project directory + * @returns Absolute path to {projectPath}/.automaker/ideation/sessions + */ +export function getIdeationSessionsDir(projectPath: string): string { + return path.join(getIdeationDir(projectPath), 'sessions'); +} + +/** + * Get the session file path for an ideation session + * + * Stores the session messages and metadata. + * + * @param projectPath - Absolute path to project directory + * @param sessionId - Session identifier + * @returns Absolute path to {projectPath}/.automaker/ideation/sessions/{sessionId}.json + */ +export function getIdeationSessionPath(projectPath: string, sessionId: string): string { + return path.join(getIdeationSessionsDir(projectPath), `${sessionId}.json`); +} + +/** + * Get the ideation drafts directory for a project + * + * Stores unsaved conversation drafts. + * + * @param projectPath - Absolute path to project directory + * @returns Absolute path to {projectPath}/.automaker/ideation/drafts + */ +export function getIdeationDraftsDir(projectPath: string): string { + return path.join(getIdeationDir(projectPath), 'drafts'); +} + +/** + * Get the project analysis result file path + * + * Stores the cached project analysis result. + * + * @param projectPath - Absolute path to project directory + * @returns Absolute path to {projectPath}/.automaker/ideation/analysis.json + */ +export function getIdeationAnalysisPath(projectPath: string): string { + return path.join(getIdeationDir(projectPath), 'analysis.json'); +} + +/** + * Create the ideation directory structure for a project if it doesn't exist + * + * Creates {projectPath}/.automaker/ideation with all subdirectories. + * Safe to call multiple times - uses recursive: true. + * + * @param projectPath - Absolute path to project directory + * @returns Promise resolving to the created ideation directory path + */ +export async function ensureIdeationDir(projectPath: string): Promise { + const ideationDir = getIdeationDir(projectPath); + await secureFs.mkdir(ideationDir, { recursive: true }); + await secureFs.mkdir(getIdeasDir(projectPath), { recursive: true }); + await secureFs.mkdir(getIdeationSessionsDir(projectPath), { recursive: true }); + await secureFs.mkdir(getIdeationDraftsDir(projectPath), { recursive: true }); + return ideationDir; +} + // ============================================================================ // Global Settings Paths (stored in DATA_DIR from app.getPath('userData')) // ============================================================================ diff --git a/libs/types/src/event.ts b/libs/types/src/event.ts index 805f48e4..091b3e90 100644 --- a/libs/types/src/event.ts +++ b/libs/types/src/event.ts @@ -26,6 +26,15 @@ export type EventType = | 'project:analysis-error' | 'suggestions:event' | 'spec-regeneration:event' - | 'issue-validation:event'; + | 'issue-validation:event' + | 'ideation:stream' + | 'ideation:session-started' + | 'ideation:session-ended' + | 'ideation:analysis' + | 'ideation:analysis-started' + | 'ideation:analysis-progress' + | 'ideation:analysis-complete' + | 'ideation:analysis-error' + | 'ideation:suggestions'; export type EventCallback = (type: EventType, payload: unknown) => void; diff --git a/libs/types/src/ideation.ts b/libs/types/src/ideation.ts new file mode 100644 index 00000000..c1c80903 --- /dev/null +++ b/libs/types/src/ideation.ts @@ -0,0 +1,230 @@ +/** + * Ideation types for AutoMaker brainstorming and idea management + */ + +// ============================================================================ +// Core Types +// ============================================================================ + +export type IdeaCategory = + | 'feature' + | 'ux-ui' + | 'dx' + | 'growth' + | 'technical' + | 'security' + | 'performance' + | 'accessibility' + | 'analytics'; +export type IdeaStatus = 'raw' | 'refined' | 'ready' | 'archived'; +export type ImpactLevel = 'low' | 'medium' | 'high'; +export type EffortLevel = 'low' | 'medium' | 'high'; + +// ============================================================================ +// Idea Entity +// ============================================================================ + +export interface IdeaAttachment { + id: string; + type: 'image' | 'link' | 'reference'; + path?: string; + url?: string; + description?: string; + [key: string]: unknown; +} + +export interface Idea { + id: string; + title: string; + description: string; + category: IdeaCategory; + status: IdeaStatus; + impact: ImpactLevel; + effort: EffortLevel; + + // Conversation context + conversationId?: string; + sourcePromptId?: string; + + // Content + attachments?: IdeaAttachment[]; + userStories?: string[]; + notes?: string; + + // Metadata + createdAt: string; + updatedAt: string; + archivedAt?: string; + + // Extensibility + [key: string]: unknown; +} + +// ============================================================================ +// Ideation Session +// ============================================================================ + +export type IdeationSessionStatus = 'active' | 'completed' | 'abandoned'; + +export interface IdeationSession { + id: string; + projectPath: string; + promptCategory?: IdeaCategory; + promptId?: string; + status: IdeationSessionStatus; + createdAt: string; + updatedAt: string; +} + +export interface IdeationMessage { + id: string; + role: 'user' | 'assistant'; + content: string; + timestamp: string; + savedAsIdeaId?: string; +} + +export interface IdeationSessionWithMessages extends IdeationSession { + messages: IdeationMessage[]; +} + +// ============================================================================ +// Guided Prompts +// ============================================================================ + +export interface PromptCategory { + id: IdeaCategory; + name: string; + icon: string; + description: string; +} + +export interface IdeationPrompt { + id: string; + category: IdeaCategory; + title: string; + description: string; + prompt: string; + icon?: string; +} + +// ============================================================================ +// Project Analysis +// ============================================================================ + +export interface AnalysisFileInfo { + path: string; + type: 'route' | 'component' | 'service' | 'model' | 'config' | 'test' | 'other'; + name: string; +} + +export interface AnalysisSuggestion { + id: string; + category: IdeaCategory; + title: string; + description: string; + rationale: string; + relatedFiles?: string[]; + priority: 'high' | 'medium' | 'low'; +} + +export interface ProjectAnalysisResult { + projectPath: string; + analyzedAt: string; + + // Structure analysis + totalFiles: number; + routes: AnalysisFileInfo[]; + components: AnalysisFileInfo[]; + services: AnalysisFileInfo[]; + + // Tech stack detection + framework?: string; + language?: string; + dependencies?: string[]; + + // AI-generated suggestions + suggestions: AnalysisSuggestion[]; + + // Summary + summary: string; +} + +// ============================================================================ +// API Types +// ============================================================================ + +export interface StartSessionOptions { + promptId?: string; + promptCategory?: IdeaCategory; + initialMessage?: string; +} + +export interface SendMessageOptions { + imagePaths?: string[]; + model?: string; +} + +export interface CreateIdeaInput { + title: string; + description: string; + category: IdeaCategory; + status?: IdeaStatus; + impact?: ImpactLevel; + effort?: EffortLevel; + conversationId?: string; + sourcePromptId?: string; + userStories?: string[]; + notes?: string; +} + +export interface UpdateIdeaInput { + title?: string; + description?: string; + category?: IdeaCategory; + status?: IdeaStatus; + impact?: ImpactLevel; + effort?: EffortLevel; + userStories?: string[]; + notes?: string; +} + +export interface ConvertToFeatureOptions { + column?: string; + dependencies?: string[]; + tags?: string[]; + keepIdea?: boolean; +} + +// ============================================================================ +// Event Types +// ============================================================================ + +export type IdeationEventType = + | 'ideation:stream' + | 'ideation:session-started' + | 'ideation:session-ended' + | 'ideation:analysis-started' + | 'ideation:analysis-progress' + | 'ideation:analysis-complete' + | 'ideation:analysis-error'; + +export interface IdeationStreamEvent { + type: 'ideation:stream'; + sessionId: string; + content: string; + done: boolean; +} + +export interface IdeationAnalysisEvent { + type: + | 'ideation:analysis-started' + | 'ideation:analysis-progress' + | 'ideation:analysis-complete' + | 'ideation:analysis-error'; + projectPath: string; + progress?: number; + message?: string; + result?: ProjectAnalysisResult; + error?: string; +} diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index be714877..62a0a9be 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -143,3 +143,30 @@ export type { // Port configuration export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './ports.js'; + +// Ideation types +export type { + IdeaCategory, + IdeaStatus, + ImpactLevel, + EffortLevel, + IdeaAttachment, + Idea, + IdeationSessionStatus, + IdeationSession, + IdeationMessage, + IdeationSessionWithMessages, + PromptCategory, + IdeationPrompt, + AnalysisFileInfo, + AnalysisSuggestion, + ProjectAnalysisResult, + StartSessionOptions, + SendMessageOptions, + CreateIdeaInput, + UpdateIdeaInput, + ConvertToFeatureOptions, + IdeationEventType, + IdeationStreamEvent, + IdeationAnalysisEvent, +} from './ideation.js';