feat: implement ideation feature for brainstorming and idea management

- Introduced a new IdeationService to manage brainstorming sessions, including idea creation, analysis, and conversion to features.
- Added RESTful API routes for ideation, including session management, idea CRUD operations, and suggestion generation.
- Created UI components for the ideation dashboard, prompt selection, and category grid to enhance user experience.
- Integrated keyboard shortcuts and navigation for the ideation feature, improving accessibility and workflow.
- Updated state management with Zustand to handle ideation-specific data and actions.
- Added necessary types and paths for ideation functionality, ensuring type safety and clarity in the codebase.
This commit is contained in:
webdevcody
2026-01-03 02:58:43 -05:00
parent 2bbc8113c0
commit ff281e23d0
44 changed files with 4495 additions and 711 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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<void> => {
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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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) });
}
};
}

View File

@@ -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<void> => {
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,
});
}
};
}

File diff suppressed because it is too large Load Diff