From a6dad428e950fbe98ef3b11284d34d586d901a5e Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Thu, 18 Dec 2025 20:09:44 +0100 Subject: [PATCH] feat: enhance roadmap generation with stop functionality and debug logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added stop functionality for roadmap generation, allowing users to halt the process. - Implemented debug logging throughout the roadmap generation process for better traceability. - Updated IPC channels to support stopping roadmap generation and added corresponding handlers. - Enhanced UI components to include stop buttons and feedback for the stop action. This update improves user control over the roadmap generation process and aids in debugging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .gitignore | 1 + auto-claude-ui/.env.example | 48 ++ auto-claude-ui/package.json | 14 +- .../src/main/agent/agent-manager.ts | 14 + auto-claude-ui/src/main/agent/agent-queue.ts | 183 ++++++- auto-claude-ui/src/main/index.ts | 15 + .../src/main/ipc-handlers/github/utils.ts | 2 +- .../ideation/generation-handlers.ts | 18 + .../src/main/ipc-handlers/roadmap-handlers.ts | 33 ++ auto-claude-ui/src/main/project-store.ts | 2 +- .../src/main/updater/http-client.ts | 40 +- .../src/preload/api/modules/roadmap-api.ts | 14 +- auto-claude-ui/src/preload/index.ts | 3 + .../renderer/components/EnvConfigModal.tsx | 462 ++++++++++++++---- .../src/renderer/components/Roadmap.tsx | 3 + .../components/RoadmapGenerationProgress.tsx | 25 +- .../components/roadmap/RoadmapHeader.tsx | 43 +- .../src/renderer/components/roadmap/hooks.ts | 15 +- .../src/renderer/components/roadmap/types.ts | 1 + .../components/settings/AdvancedSettings.tsx | 2 +- auto-claude-ui/src/renderer/hooks/useIpc.ts | 36 ++ .../src/renderer/stores/ideation-store.ts | 89 +++- .../src/renderer/stores/roadmap-store.ts | 44 +- auto-claude-ui/src/shared/constants/ipc.ts | 2 + auto-claude-ui/src/shared/types/ipc.ts | 8 +- auto-claude-ui/src/shared/types/task.ts | 5 +- auto-claude/ideation/analyzer.py | 8 +- auto-claude/ideation/phase_executor.py | 4 +- auto-claude/implementation_plan/plan.py | 5 +- auto-claude/prompts/planner.md | 1 + 30 files changed, 982 insertions(+), 158 deletions(-) create mode 100644 auto-claude-ui/.env.example diff --git a/.gitignore b/.gitignore index 3a45f4e0..00e27358 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ dmypy.json .auto-claude-security.json .auto-claude-status .claude_settings.json +.update-metadata.json # Development of Auto Build with Auto Build dev/ diff --git a/auto-claude-ui/.env.example b/auto-claude-ui/.env.example new file mode 100644 index 00000000..82207fcb --- /dev/null +++ b/auto-claude-ui/.env.example @@ -0,0 +1,48 @@ +# Auto Claude UI Environment Variables +# Copy this file to .env and set your values + +# ============================================ +# DEBUG SETTINGS +# ============================================ + +# Enable general debug logging for ideation and roadmap features +# When enabled, you'll see detailed console logs for: +# - Ideation generation and stop functionality +# - Roadmap generation and stop functionality +# - IPC communication between processes +# - Store state updates +# Usage: Set to 'true' before starting the app +# DEBUG=true + +# Enable debug logging for the auto-updater +# Shows detailed information about app update checks and downloads +# DEBUG_UPDATER=true + +# Enable debug logging for Auto Claude features +# Affects changelog generation, project initialization, and other core features +# AUTO_CLAUDE_DEBUG=true + +# ============================================ +# HOW TO USE +# ============================================ + +# Option 1: Set in your shell before starting the app +# DEBUG=true npm start +# +# Option 2: Export in your shell profile (~/.bashrc, ~/.zshrc, etc.) +# export DEBUG=true +# export AUTO_CLAUDE_DEBUG=true +# +# Option 3: Create a .env file in this directory (auto-claude-ui/) +# Copy this file: cp .env.example .env +# Then uncomment and set the variables you need +# +# Note: The Electron app will read these from process.env +# The Python backend (auto-claude) has its own .env file + +# ============================================ +# DEVELOPMENT +# ============================================ + +# Node environment (automatically set by npm scripts) +# NODE_ENV=development diff --git a/auto-claude-ui/package.json b/auto-claude-ui/package.json index 3d5ebb96..056bc99b 100644 --- a/auto-claude-ui/package.json +++ b/auto-claude-ui/package.json @@ -1,6 +1,6 @@ { "name": "auto-claude-ui", - "version": "2.3.0", + "version": "2.5.0", "description": "Desktop UI for Auto Claude autonomous coding framework", "main": "./out/main/index.js", "author": "Auto Claude Team", @@ -138,6 +138,18 @@ { "from": "resources/icon.ico", "to": "icon.ico" + }, + { + "from": "../auto-claude", + "to": "auto-claude", + "filter": [ + "!**/.git", + "!**/__pycache__", + "!**/*.pyc", + "!**/specs", + "!**/.venv", + "!**/.env" + ] } ], "mac": { diff --git a/auto-claude-ui/src/main/agent/agent-manager.ts b/auto-claude-ui/src/main/agent/agent-manager.ts index aee9bafe..7a39ef30 100644 --- a/auto-claude-ui/src/main/agent/agent-manager.ts +++ b/auto-claude-ui/src/main/agent/agent-manager.ts @@ -250,6 +250,20 @@ export class AgentManager extends EventEmitter { return this.queueManager.isIdeationRunning(projectId); } + /** + * Stop roadmap generation for a project + */ + stopRoadmap(projectId: string): boolean { + return this.queueManager.stopRoadmap(projectId); + } + + /** + * Check if roadmap is running for a project + */ + isRoadmapRunning(projectId: string): boolean { + return this.queueManager.isRoadmapRunning(projectId); + } + /** * Kill all running processes */ diff --git a/auto-claude-ui/src/main/agent/agent-queue.ts b/auto-claude-ui/src/main/agent/agent-queue.ts index 9fa8ba48..c864d964 100644 --- a/auto-claude-ui/src/main/agent/agent-queue.ts +++ b/auto-claude-ui/src/main/agent/agent-queue.ts @@ -7,6 +7,7 @@ import { AgentEvents } from './agent-events'; import { AgentProcessManager } from './agent-process'; import { IdeationConfig } from './types'; import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector'; +import { debugLog, debugError } from '../../shared/utils/debug-logger'; /** * Queue management for ideation and roadmap generation @@ -38,16 +39,25 @@ export class AgentQueueManager { refresh: boolean = false, enableCompetitorAnalysis: boolean = false ): void { + debugLog('[Agent Queue] Starting roadmap generation:', { + projectId, + projectPath, + refresh, + enableCompetitorAnalysis + }); + const autoBuildSource = this.processManager.getAutoBuildSourcePath(); if (!autoBuildSource) { + debugError('[Agent Queue] Auto-build source path not found'); this.emitter.emit('roadmap-error', projectId, 'Auto-build source path not found. Please configure it in App Settings.'); return; } - const roadmapRunnerPath = path.join(autoBuildSource, 'roadmap_runner.py'); + const roadmapRunnerPath = path.join(autoBuildSource, 'runners', 'roadmap_runner.py'); if (!existsSync(roadmapRunnerPath)) { + debugError('[Agent Queue] Roadmap runner not found at:', roadmapRunnerPath); this.emitter.emit('roadmap-error', projectId, `Roadmap runner not found at: ${roadmapRunnerPath}`); return; } @@ -63,6 +73,8 @@ export class AgentQueueManager { args.push('--competitor-analysis'); } + debugLog('[Agent Queue] Spawning roadmap process with args:', args); + // Use projectId as taskId for roadmap operations this.spawnRoadmapProcess(projectId, projectPath, args); } @@ -76,16 +88,25 @@ export class AgentQueueManager { config: IdeationConfig, refresh: boolean = false ): void { + debugLog('[Agent Queue] Starting ideation generation:', { + projectId, + projectPath, + config, + refresh + }); + const autoBuildSource = this.processManager.getAutoBuildSourcePath(); if (!autoBuildSource) { + debugError('[Agent Queue] Auto-build source path not found'); this.emitter.emit('ideation-error', projectId, 'Auto-build source path not found. Please configure it in App Settings.'); return; } - const ideationRunnerPath = path.join(autoBuildSource, 'ideation_runner.py'); + const ideationRunnerPath = path.join(autoBuildSource, 'runners', 'ideation_runner.py'); if (!existsSync(ideationRunnerPath)) { + debugError('[Agent Queue] Ideation runner not found at:', ideationRunnerPath); this.emitter.emit('ideation-error', projectId, `Ideation runner not found at: ${ideationRunnerPath}`); return; } @@ -119,6 +140,8 @@ export class AgentQueueManager { args.push('--append'); } + debugLog('[Agent Queue] Spawning ideation process with args:', args); + // Use projectId as taskId for ideation operations this.spawnIdeationProcess(projectId, projectPath, args); } @@ -131,11 +154,17 @@ export class AgentQueueManager { projectPath: string, args: string[] ): void { + debugLog('[Agent Queue] Spawning ideation process:', { projectId, projectPath }); + // Kill existing process for this project if any - this.processManager.killProcess(projectId); + const wasKilled = this.processManager.killProcess(projectId); + if (wasKilled) { + debugLog('[Agent Queue] Killed existing process for project:', projectId); + } // Generate unique spawn ID for this process instance const spawnId = this.state.generateSpawnId(); + debugLog('[Agent Queue] Generated spawn ID:', spawnId); // Run from auto-claude source directory so imports work correctly const autoBuildSource = this.processManager.getAutoBuildSourcePath(); @@ -144,22 +173,41 @@ export class AgentQueueManager { // Get combined environment variables const combinedEnv = this.processManager.getCombinedEnv(projectPath); - // Get active Claude profile environment (CLAUDE_CONFIG_DIR if not default) + // Get active Claude profile environment (CLAUDE_CODE_OAUTH_TOKEN if not default) const profileEnv = getProfileEnv(); // Get Python path from process manager (uses venv if configured) const pythonPath = this.processManager.getPythonPath(); + // Build final environment with proper precedence: + // 1. process.env (system) + // 2. combinedEnv (auto-claude/.env for CLI usage) + // 3. profileEnv (Electron app OAuth token - highest priority) + // 4. Our specific overrides + const finalEnv = { + ...process.env, + ...combinedEnv, + ...profileEnv, + PYTHONPATH: autoBuildSource || '', // Allow imports from auto-claude directory + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' + }; + + // Debug: Show OAuth token source + const tokenSource = profileEnv.CLAUDE_CODE_OAUTH_TOKEN + ? 'Electron app profile' + : (combinedEnv.CLAUDE_CODE_OAUTH_TOKEN ? 'auto-claude/.env' : 'not found'); + const hasToken = !!finalEnv.CLAUDE_CODE_OAUTH_TOKEN; + debugLog('[Agent Queue] OAuth token status:', { + source: tokenSource, + hasToken, + tokenPreview: hasToken ? finalEnv.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...' : 'none' + }); + const childProcess = spawn(pythonPath, args, { cwd, - env: { - ...process.env, - ...combinedEnv, - ...profileEnv, - PYTHONUNBUFFERED: '1', - PYTHONIOENCODING: 'utf-8', - PYTHONUTF8: '1' - } + env: finalEnv }); this.state.addProcess(projectId, { @@ -206,6 +254,13 @@ export class AgentQueueManager { const [, ideationType, ideasCount] = typeCompleteMatch; completedTypes.add(ideationType); + debugLog('[Agent Queue] Ideation type completed:', { + projectId, + ideationType, + ideasCount: parseInt(ideasCount, 10), + totalCompleted: completedTypes.size + }); + // Emit event for UI to load this type's ideas immediately this.emitter.emit('ideation-type-complete', projectId, ideationType, parseInt(ideasCount, 10)); } @@ -214,6 +269,8 @@ export class AgentQueueManager { if (typeFailedMatch) { const [, ideationType] = typeFailedMatch; completedTypes.add(ideationType); + + debugError('[Agent Queue] Ideation type failed:', { projectId, ideationType }); this.emitter.emit('ideation-type-failed', projectId, ideationType); } @@ -254,6 +311,8 @@ export class AgentQueueManager { // Handle process exit childProcess.on('exit', (code: number | null) => { + debugLog('[Agent Queue] Ideation process exited:', { projectId, code, spawnId }); + // Get the stored project path before deleting from map const processInfo = this.state.getProcess(projectId); const storedProjectPath = processInfo?.projectPath; @@ -261,8 +320,10 @@ export class AgentQueueManager { // Check for rate limit if process failed if (code !== 0) { + debugLog('[Agent Queue] Checking for rate limit (non-zero exit)'); const rateLimitDetection = detectRateLimit(allOutput); if (rateLimitDetection.isRateLimited) { + debugLog('[Agent Queue] Rate limit detected for ideation'); const rateLimitInfo = createSDKRateLimitInfo('ideation', rateLimitDetection, { projectId }); @@ -271,6 +332,7 @@ export class AgentQueueManager { } if (code === 0) { + debugLog('[Agent Queue] Ideation generation completed successfully'); this.emitter.emit('ideation-progress', projectId, { phase: 'complete', progress: 100, @@ -286,18 +348,25 @@ export class AgentQueueManager { 'ideation', 'ideation.json' ); + debugLog('[Agent Queue] Loading ideation session from:', ideationFilePath); if (existsSync(ideationFilePath)) { const content = readFileSync(ideationFilePath, 'utf-8'); const session = JSON.parse(content); + debugLog('[Agent Queue] Loaded ideation session:', { + totalIdeas: session.ideas?.length || 0 + }); this.emitter.emit('ideation-complete', projectId, session); } else { + debugError('[Ideation] ideation.json not found at:', ideationFilePath); console.warn('[Ideation] ideation.json not found at:', ideationFilePath); } } catch (err) { + debugError('[Ideation] Failed to load ideation session:', err); console.error('[Ideation] Failed to load ideation session:', err); } } } else { + debugError('[Agent Queue] Ideation generation failed:', { projectId, code }); this.emitter.emit('ideation-error', projectId, `Ideation generation failed with exit code ${code}`); } }); @@ -318,11 +387,17 @@ export class AgentQueueManager { projectPath: string, args: string[] ): void { + debugLog('[Agent Queue] Spawning roadmap process:', { projectId, projectPath }); + // Kill existing process for this project if any - this.processManager.killProcess(projectId); + const wasKilled = this.processManager.killProcess(projectId); + if (wasKilled) { + debugLog('[Agent Queue] Killed existing roadmap process for project:', projectId); + } // Generate unique spawn ID for this process instance const spawnId = this.state.generateSpawnId(); + debugLog('[Agent Queue] Generated roadmap spawn ID:', spawnId); // Run from auto-claude source directory so imports work correctly const autoBuildSource = this.processManager.getAutoBuildSourcePath(); @@ -331,22 +406,41 @@ export class AgentQueueManager { // Get combined environment variables const combinedEnv = this.processManager.getCombinedEnv(projectPath); - // Get active Claude profile environment (CLAUDE_CONFIG_DIR if not default) + // Get active Claude profile environment (CLAUDE_CODE_OAUTH_TOKEN if not default) const profileEnv = getProfileEnv(); // Get Python path from process manager (uses venv if configured) const pythonPath = this.processManager.getPythonPath(); + // Build final environment with proper precedence: + // 1. process.env (system) + // 2. combinedEnv (auto-claude/.env for CLI usage) + // 3. profileEnv (Electron app OAuth token - highest priority) + // 4. Our specific overrides + const finalEnv = { + ...process.env, + ...combinedEnv, + ...profileEnv, + PYTHONPATH: autoBuildSource || '', // Allow imports from auto-claude directory + PYTHONUNBUFFERED: '1', + PYTHONIOENCODING: 'utf-8', + PYTHONUTF8: '1' + }; + + // Debug: Show OAuth token source + const tokenSource = profileEnv.CLAUDE_CODE_OAUTH_TOKEN + ? 'Electron app profile' + : (combinedEnv.CLAUDE_CODE_OAUTH_TOKEN ? 'auto-claude/.env' : 'not found'); + const hasToken = !!finalEnv.CLAUDE_CODE_OAUTH_TOKEN; + debugLog('[Agent Queue] OAuth token status:', { + source: tokenSource, + hasToken, + tokenPreview: hasToken ? finalEnv.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...' : 'none' + }); + const childProcess = spawn(pythonPath, args, { cwd, - env: { - ...process.env, - ...combinedEnv, - ...profileEnv, - PYTHONUNBUFFERED: '1', - PYTHONIOENCODING: 'utf-8', - PYTHONUTF8: '1' - } + env: finalEnv }); this.state.addProcess(projectId, { @@ -412,6 +506,8 @@ export class AgentQueueManager { // Handle process exit childProcess.on('exit', (code: number | null) => { + debugLog('[Agent Queue] Roadmap process exited:', { projectId, code, spawnId }); + // Get the stored project path before deleting from map const processInfo = this.state.getProcess(projectId); const storedProjectPath = processInfo?.projectPath; @@ -419,8 +515,10 @@ export class AgentQueueManager { // Check for rate limit if process failed if (code !== 0) { + debugLog('[Agent Queue] Checking for rate limit (non-zero exit)'); const rateLimitDetection = detectRateLimit(allRoadmapOutput); if (rateLimitDetection.isRateLimited) { + debugLog('[Agent Queue] Rate limit detected for roadmap'); const rateLimitInfo = createSDKRateLimitInfo('roadmap', rateLimitDetection, { projectId }); @@ -429,6 +527,7 @@ export class AgentQueueManager { } if (code === 0) { + debugLog('[Agent Queue] Roadmap generation completed successfully'); this.emitter.emit('roadmap-progress', projectId, { phase: 'complete', progress: 100, @@ -444,18 +543,26 @@ export class AgentQueueManager { 'roadmap', 'roadmap.json' ); + debugLog('[Agent Queue] Loading roadmap from:', roadmapFilePath); if (existsSync(roadmapFilePath)) { const content = readFileSync(roadmapFilePath, 'utf-8'); const roadmap = JSON.parse(content); + debugLog('[Agent Queue] Loaded roadmap:', { + featuresCount: roadmap.features?.length || 0, + phasesCount: roadmap.phases?.length || 0 + }); this.emitter.emit('roadmap-complete', projectId, roadmap); } else { + debugError('[Roadmap] roadmap.json not found at:', roadmapFilePath); console.warn('[Roadmap] roadmap.json not found at:', roadmapFilePath); } } catch (err) { + debugError('[Roadmap] Failed to load roadmap:', err); console.error('[Roadmap] Failed to load roadmap:', err); } } } else { + debugError('[Agent Queue] Roadmap generation failed:', { projectId, code }); this.emitter.emit('roadmap-error', projectId, `Roadmap generation failed with exit code ${code}`); } }); @@ -472,12 +579,18 @@ export class AgentQueueManager { * Stop ideation generation for a project */ stopIdeation(projectId: string): boolean { + debugLog('[Agent Queue] Stop ideation requested:', { projectId }); + const wasRunning = this.state.hasProcess(projectId); + debugLog('[Agent Queue] Process running?', { projectId, wasRunning }); + if (wasRunning) { + debugLog('[Agent Queue] Killing ideation process:', projectId); this.processManager.killProcess(projectId); this.emitter.emit('ideation-stopped', projectId); return true; } + debugLog('[Agent Queue] No running process found for:', projectId); return false; } @@ -487,4 +600,30 @@ export class AgentQueueManager { isIdeationRunning(projectId: string): boolean { return this.state.hasProcess(projectId); } + + /** + * Stop roadmap generation for a project + */ + stopRoadmap(projectId: string): boolean { + debugLog('[Agent Queue] Stop roadmap requested:', { projectId }); + + const wasRunning = this.state.hasProcess(projectId); + debugLog('[Agent Queue] Roadmap process running?', { projectId, wasRunning }); + + if (wasRunning) { + debugLog('[Agent Queue] Killing roadmap process:', projectId); + this.processManager.killProcess(projectId); + this.emitter.emit('roadmap-stopped', projectId); + return true; + } + debugLog('[Agent Queue] No running roadmap process found for:', projectId); + return false; + } + + /** + * Check if roadmap is running for a project + */ + isRoadmapRunning(projectId: string): boolean { + return this.state.hasProcess(projectId); + } } diff --git a/auto-claude-ui/src/main/index.ts b/auto-claude-ui/src/main/index.ts index bd7b4d9a..0b4c4a53 100644 --- a/auto-claude-ui/src/main/index.ts +++ b/auto-claude-ui/src/main/index.ts @@ -139,6 +139,21 @@ app.whenReady().then(() => { usageMonitor.start(); console.warn('[main] Usage monitor initialized and started'); + // Log debug mode status + const isDebugMode = process.env.DEBUG === 'true'; + const isAutoClaudeDebug = process.env.AUTO_CLAUDE_DEBUG === 'true'; + if (isDebugMode || isAutoClaudeDebug) { + console.warn('[main] ========================================'); + console.warn('[main] DEBUG MODE ENABLED'); + if (isDebugMode) { + console.warn('[main] - DEBUG=true (Ideation/Roadmap debug logging)'); + } + if (isAutoClaudeDebug) { + console.warn('[main] - AUTO_CLAUDE_DEBUG=true (Core features debug logging)'); + } + console.warn('[main] ========================================'); + } + // Initialize app auto-updater (only in production, or when DEBUG_UPDATER is set) const forceUpdater = process.env.DEBUG_UPDATER === 'true'; if (app.isPackaged || forceUpdater) { diff --git a/auto-claude-ui/src/main/ipc-handlers/github/utils.ts b/auto-claude-ui/src/main/ipc-handlers/github/utils.ts index 1e0a4285..d25da8cb 100644 --- a/auto-claude-ui/src/main/ipc-handlers/github/utils.ts +++ b/auto-claude-ui/src/main/ipc-handlers/github/utils.ts @@ -69,7 +69,7 @@ export async function githubFetch( const response = await fetch(url, { ...options, headers: { - 'Accept': 'application/vnd.github.v3+json', + 'Accept': 'application/vnd.github+json', 'Authorization': `Bearer ${token}`, 'User-Agent': 'Auto-Claude-UI', ...options.headers diff --git a/auto-claude-ui/src/main/ipc-handlers/ideation/generation-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/ideation/generation-handlers.ts index d38be5ce..57a12bd6 100644 --- a/auto-claude-ui/src/main/ipc-handlers/ideation/generation-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/ideation/generation-handlers.ts @@ -7,6 +7,7 @@ import { IPC_CHANNELS } from '../../../shared/constants'; import type { IPCResult, IdeationConfig, IdeationGenerationStatus } from '../../../shared/types'; import { projectStore } from '../../project-store'; import type { AgentManager } from '../../agent'; +import { debugLog } from '../../../shared/utils/debug-logger'; /** * Start ideation generation for a project @@ -18,10 +19,17 @@ export function startIdeationGeneration( agentManager: AgentManager, mainWindow: BrowserWindow | null ): void { + debugLog('[Ideation Handler] Start generation request:', { + projectId, + enabledTypes: config.enabledTypes, + maxIdeasPerType: config.maxIdeasPerType + }); + if (!mainWindow) return; const project = projectStore.getProject(projectId); if (!project) { + debugLog('[Ideation Handler] Project not found:', projectId); mainWindow.webContents.send( IPC_CHANNELS.IDEATION_ERROR, projectId, @@ -30,6 +38,11 @@ export function startIdeationGeneration( return; } + debugLog('[Ideation Handler] Starting agent manager generation:', { + projectId, + projectPath: project.path + }); + // Start ideation generation via agent manager agentManager.startIdeationGeneration(projectId, project.path, config, false); @@ -91,9 +104,14 @@ export async function stopIdeationGeneration( agentManager: AgentManager, mainWindow: BrowserWindow | null ): Promise { + debugLog('[Ideation Handler] Stop generation request:', { projectId }); + const wasStopped = agentManager.stopIdeation(projectId); + debugLog('[Ideation Handler] Stop result:', { projectId, wasStopped }); + if (wasStopped && mainWindow) { + debugLog('[Ideation Handler] Sending stopped event to renderer'); mainWindow.webContents.send(IPC_CHANNELS.IDEATION_STOPPED, projectId); } diff --git a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts index 86bad572..30d2ee7e 100644 --- a/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/roadmap-handlers.ts @@ -6,6 +6,7 @@ import path from 'path'; import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; import { projectStore } from '../project-store'; import { AgentManager } from '../agent'; +import { debugLog, debugError } from '../../shared/utils/debug-logger'; /** @@ -162,11 +163,17 @@ export function registerRoadmapHandlers( ipcMain.on( IPC_CHANNELS.ROADMAP_GENERATE, (_, projectId: string, enableCompetitorAnalysis?: boolean) => { + debugLog('[Roadmap Handler] Generate request:', { + projectId, + enableCompetitorAnalysis + }); + const mainWindow = getMainWindow(); if (!mainWindow) return; const project = projectStore.getProject(projectId); if (!project) { + debugError('[Roadmap Handler] Project not found:', projectId); mainWindow.webContents.send( IPC_CHANNELS.ROADMAP_ERROR, projectId, @@ -175,6 +182,11 @@ export function registerRoadmapHandlers( return; } + debugLog('[Roadmap Handler] Starting agent manager generation:', { + projectId, + projectPath: project.path + }); + // Start roadmap generation via agent manager agentManager.startRoadmapGeneration(projectId, project.path, false, enableCompetitorAnalysis ?? false); @@ -223,6 +235,27 @@ export function registerRoadmapHandlers( } ); + ipcMain.handle( + IPC_CHANNELS.ROADMAP_STOP, + async (_, projectId: string): Promise => { + debugLog('[Roadmap Handler] Stop generation request:', { projectId }); + + const mainWindow = getMainWindow(); + + // Stop roadmap generation for this project + const wasStopped = agentManager.stopRoadmap(projectId); + + debugLog('[Roadmap Handler] Stop result:', { projectId, wasStopped }); + + if (wasStopped && mainWindow) { + debugLog('[Roadmap Handler] Sending stopped event to renderer'); + mainWindow.webContents.send(IPC_CHANNELS.ROADMAP_STOPPED, projectId); + } + + return { success: wasStopped }; + } + ); + // ============================================ // Roadmap Save (full state persistence for drag-and-drop) // ============================================ diff --git a/auto-claude-ui/src/main/project-store.ts b/auto-claude-ui/src/main/project-store.ts index e77207fb..5ee38bad 100644 --- a/auto-claude-ui/src/main/project-store.ts +++ b/auto-claude-ui/src/main/project-store.ts @@ -294,7 +294,7 @@ export class ProjectStore { id: dir.name, // Use spec directory name as ID specId: dir.name, projectId, - title: plan?.feature || dir.name, + title: plan?.feature || plan?.title || dir.name, description, status, reviewReason, diff --git a/auto-claude-ui/src/main/updater/http-client.ts b/auto-claude-ui/src/main/updater/http-client.ts index 8d8a91da..69b4d469 100644 --- a/auto-claude-ui/src/main/updater/http-client.ts +++ b/auto-claude-ui/src/main/updater/http-client.ts @@ -11,12 +11,12 @@ import { TIMEOUTS } from './config'; */ export function fetchJson(url: string): Promise { return new Promise((resolve, reject) => { - const request = https.get(url, { - headers: { - 'User-Agent': 'Auto-Claude-UI', - 'Accept': 'application/vnd.github.v3+json' - } - }, (response) => { + const headers = { + 'User-Agent': 'Auto-Claude-UI', + 'Accept': 'application/vnd.github+json' + }; + + const request = https.get(url, { headers }, (response) => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; @@ -27,7 +27,13 @@ export function fetchJson(url: string): Promise { } if (response.statusCode !== 200) { - reject(new Error(`HTTP ${response.statusCode}`)); + // Collect response body for error details + let errorData = ''; + response.on('data', chunk => errorData += chunk); + response.on('end', () => { + const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`; + reject(new Error(errorMsg)); + }); return; } @@ -62,12 +68,12 @@ export function downloadFile( return new Promise((resolve, reject) => { const file = createWriteStream(destPath); - const request = https.get(url, { - headers: { - 'User-Agent': 'Auto-Claude-UI', - 'Accept': 'application/octet-stream' - } - }, (response) => { + const headers = { + 'User-Agent': 'Auto-Claude-UI', + 'Accept': 'application/vnd.github+json' + }; + + const request = https.get(url, { headers }, (response) => { // Handle redirects if (response.statusCode === 301 || response.statusCode === 302) { file.close(); @@ -80,7 +86,13 @@ export function downloadFile( if (response.statusCode !== 200) { file.close(); - reject(new Error(`HTTP ${response.statusCode}`)); + // Collect response body for error details + let errorData = ''; + response.on('data', chunk => errorData += chunk); + response.on('end', () => { + const errorMsg = `HTTP ${response.statusCode}: ${errorData || response.statusMessage || 'No error details'}`; + reject(new Error(errorMsg)); + }); return; } diff --git a/auto-claude-ui/src/preload/api/modules/roadmap-api.ts b/auto-claude-ui/src/preload/api/modules/roadmap-api.ts index cb400092..1290e1e8 100644 --- a/auto-claude-ui/src/preload/api/modules/roadmap-api.ts +++ b/auto-claude-ui/src/preload/api/modules/roadmap-api.ts @@ -17,6 +17,7 @@ export interface RoadmapAPI { saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise; generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; + stopRoadmap: (projectId: string) => Promise; updateFeatureStatus: ( projectId: string, featureId: string, @@ -37,6 +38,9 @@ export interface RoadmapAPI { onRoadmapError: ( callback: (projectId: string, error: string) => void ) => IpcListenerCleanup; + onRoadmapStopped: ( + callback: (projectId: string) => void + ) => IpcListenerCleanup; } /** @@ -56,6 +60,9 @@ export const createRoadmapAPI = (): RoadmapAPI => ({ refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean): void => sendIpc(IPC_CHANNELS.ROADMAP_REFRESH, projectId, enableCompetitorAnalysis), + stopRoadmap: (projectId: string): Promise => + invokeIpc(IPC_CHANNELS.ROADMAP_STOP, projectId), + updateFeatureStatus: ( projectId: string, featureId: string, @@ -83,5 +90,10 @@ export const createRoadmapAPI = (): RoadmapAPI => ({ onRoadmapError: ( callback: (projectId: string, error: string) => void ): IpcListenerCleanup => - createIpcListener(IPC_CHANNELS.ROADMAP_ERROR, callback) + createIpcListener(IPC_CHANNELS.ROADMAP_ERROR, callback), + + onRoadmapStopped: ( + callback: (projectId: string) => void + ): IpcListenerCleanup => + createIpcListener(IPC_CHANNELS.ROADMAP_STOPPED, callback) }); diff --git a/auto-claude-ui/src/preload/index.ts b/auto-claude-ui/src/preload/index.ts index a2512a91..addd4fc0 100644 --- a/auto-claude-ui/src/preload/index.ts +++ b/auto-claude-ui/src/preload/index.ts @@ -6,3 +6,6 @@ const electronAPI = createElectronAPI(); // Expose to renderer via contextBridge contextBridge.exposeInMainWorld('electronAPI', electronAPI); + +// Expose debug flag for debug logging +contextBridge.exposeInMainWorld('DEBUG', process.env.DEBUG === 'true'); diff --git a/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx b/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx index ea26bf63..5ae2eadf 100644 --- a/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx +++ b/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx @@ -8,7 +8,10 @@ import { Copy, Eye, EyeOff, - Info + Info, + LogIn, + ChevronDown, + ChevronRight } from 'lucide-react'; import { Dialog, @@ -44,46 +47,168 @@ export function EnvConfigModal({ }: EnvConfigModalProps) { const [token, setToken] = useState(''); const [showToken, setShowToken] = useState(false); - const [_isLoading, _setIsLoading] = useState(false); + const [showManualEntry, setShowManualEntry] = useState(false); + const [isAuthenticating, setIsAuthenticating] = useState(false); const [isChecking, setIsChecking] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [sourcePath, setSourcePath] = useState(null); const [hasExistingToken, setHasExistingToken] = useState(false); + const [claudeProfiles, setClaudeProfiles] = useState>([]); + const [selectedProfileId, setSelectedProfileId] = useState(null); + const [isLoadingProfiles, setIsLoadingProfiles] = useState(true); - // Check current token status when modal opens + // Load Claude profiles and check token status when modal opens useEffect(() => { - const checkToken = async () => { + const loadData = async () => { if (!open) return; setIsChecking(true); + setIsLoadingProfiles(true); setError(null); setSuccess(false); try { - const result = await window.electronAPI.checkSourceToken(); - if (result.success && result.data) { - setSourcePath(result.data.sourcePath || null); - setHasExistingToken(result.data.hasToken); + // Load both token status and Claude profiles in parallel + const [tokenResult, profilesResult] = await Promise.all([ + window.electronAPI.checkSourceToken(), + window.electronAPI.getClaudeProfiles() + ]); - if (result.data.hasToken) { + // Handle token status + if (tokenResult.success && tokenResult.data) { + setSourcePath(tokenResult.data.sourcePath || null); + setHasExistingToken(tokenResult.data.hasToken); + + if (tokenResult.data.hasToken) { // Token exists, show success state setSuccess(true); } } else { - setError(result.error || 'Failed to check token status'); + setError(tokenResult.error || 'Failed to check token status'); + } + + // Handle Claude profiles + if (profilesResult.success && profilesResult.data) { + const authenticatedProfiles = profilesResult.data.profiles.filter( + (p: any) => p.oauthToken || (p.isDefault && p.configDir) + ); + setClaudeProfiles(authenticatedProfiles); + + // Auto-select first authenticated profile + if (authenticatedProfiles.length > 0 && !selectedProfileId) { + setSelectedProfileId(authenticatedProfiles[0].id); + } } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setIsChecking(false); + setIsLoadingProfiles(false); } }; - checkToken(); + loadData(); }, [open]); + // Listen for OAuth token from terminal + useEffect(() => { + if (!open) return; + + const cleanup = window.electronAPI.onTerminalOAuthToken(async (info) => { + if (info.success && info.token) { + // Save the OAuth token + try { + const result = await window.electronAPI.updateSourceEnv({ + claudeOAuthToken: info.token + }); + + if (result.success) { + setSuccess(true); + setHasExistingToken(true); + setIsAuthenticating(false); + + // Notify parent + setTimeout(() => { + onConfigured?.(); + onOpenChange(false); + }, 1500); + } + } catch (err) { + setError('Failed to save authentication token'); + setIsAuthenticating(false); + } + } + }); + + return cleanup; + }, [open, onConfigured, onOpenChange]); + + const handleUseExistingProfile = async () => { + if (!selectedProfileId) return; + + setIsSaving(true); + setError(null); + + try { + // Get the selected profile's token + const profile = claudeProfiles.find(p => p.id === selectedProfileId); + if (!profile?.oauthToken) { + setError('Selected profile does not have a valid token'); + setIsSaving(false); + return; + } + + // Save the token to auto-claude .env + const result = await window.electronAPI.updateSourceEnv({ + claudeOAuthToken: profile.oauthToken + }); + + if (result.success) { + setSuccess(true); + setHasExistingToken(true); + + // Notify parent + setTimeout(() => { + onConfigured?.(); + onOpenChange(false); + }, 1500); + } else { + setError(result.error || 'Failed to save token'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsSaving(false); + } + }; + + const handleAuthenticateWithBrowser = async () => { + setIsAuthenticating(true); + setError(null); + + try { + // Invoke the Claude setup-token flow in terminal + const result = await window.electronAPI.invokeClaudeSetup(); + + if (!result.success) { + setError(result.error || 'Failed to start authentication'); + setIsAuthenticating(false); + } + // Keep isAuthenticating true - will be cleared when token is received + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to start authentication'); + setIsAuthenticating(false); + } + }; + const handleSave = async () => { if (!token.trim()) { setError('Please enter a token'); @@ -182,89 +307,258 @@ export function EnvConfigModal({ )} - {/* Info about getting a token */} -
-
- -
-

- How to get a Claude Code OAuth token: -

-
    -
  1. Install Claude Code CLI if you haven't already
  2. -
  3. - Run{' '} - - claude setup-token - - {' '} + {/* Option 1: Use existing authenticated profile */} + {!isLoadingProfiles && claudeProfiles.length > 0 && ( +
    +
    +
    + +
    +

    + Use Existing Account +

    +

    + You have {claudeProfiles.length} authenticated Claude account{claudeProfiles.length > 1 ? 's' : ''}. Select one to use: +

    +
    +
    +
    + + {/* Profile selector */} +
    + +
    + {claudeProfiles.map((profile) => ( -
  4. -
  5. Copy the token and paste it below
  6. -
- + ))} +
+
+ + + + {/* Divider */} +
+
+
+
+
+ or +
- + )} - {/* Token input */} -
- -
- setToken(e.target.value)} - placeholder="Enter your token..." - className="pr-10 font-mono text-sm" - disabled={isSaving} - /> - - - - - - {showToken ? 'Hide token' : 'Show token'} - - + {/* Option 2: Authenticate new account with browser */} + {!isLoadingProfiles && ( +
+
+
+ +
+

+ {claudeProfiles.length > 0 ? 'Or Authenticate New Account' : 'Authenticate with Browser'} +

+

+ {claudeProfiles.length > 0 + ? 'Add a new Claude account by logging in with your browser.' + : 'Click below to open your browser and log in with your Claude account.' + } +

+
+
+
+ + + + {isAuthenticating && ( +

+ A browser window should open. Complete the authentication there, then return here. +

+ )}
-

- The token will be saved to{' '} - - {sourcePath ? `${sourcePath}/.env` : 'auto-claude/.env'} - -

+ )} + + {/* Divider before manual entry */} + {!isLoadingProfiles && ( +
+
+
+
+
+ or +
+
+ )} + + {/* Secondary: Manual Token Entry (Collapsible) */} +
+ + + {showManualEntry && ( +
+ {/* Manual token instructions */} +
+

Steps:

+
    +
  1. Install Claude Code CLI if you haven't already
  2. +
  3. + Run{' '} + + claude setup-token + + {' '} + +
  4. +
  5. Copy the token and paste it below
  6. +
+ +
+ + {/* Token input */} +
+ +
+ setToken(e.target.value)} + placeholder="Enter your token..." + className="pr-10 font-mono text-sm" + disabled={isSaving || isAuthenticating} + /> + + + + + + {showToken ? 'Hide token' : 'Show token'} + + +
+

+ The token will be saved to{' '} + + {sourcePath ? `${sourcePath}/.env` : 'auto-claude/.env'} + +

+
+
+ )}
{/* Existing token info */} {hasExistingToken && (

- A token is already configured. Enter a new token above to replace it. + A token is already configured. {showManualEntry ? 'Enter a new token above to replace it.' : 'Authenticate again to replace it.'}

)} @@ -272,11 +566,11 @@ export function EnvConfigModal({ )} - - {!success && ( -
); @@ -73,6 +75,7 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) { {/* Header */} setShowAddFeatureDialog(true)} onRefresh={handleRefresh} /> diff --git a/auto-claude-ui/src/renderer/components/RoadmapGenerationProgress.tsx b/auto-claude-ui/src/renderer/components/RoadmapGenerationProgress.tsx index 29a215ed..ef26ac1f 100644 --- a/auto-claude-ui/src/renderer/components/RoadmapGenerationProgress.tsx +++ b/auto-claude-ui/src/renderer/components/RoadmapGenerationProgress.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'motion/react'; -import { Search, Users, Sparkles, CheckCircle2, AlertCircle } from 'lucide-react'; +import { Search, Users, Sparkles, CheckCircle2, AlertCircle, Square } from 'lucide-react'; +import { Button } from './ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { cn } from '../lib/utils'; import type { RoadmapGenerationStatus } from '../../shared/types/roadmap'; @@ -36,6 +38,7 @@ function useReducedMotion(): boolean { interface RoadmapGenerationProgressProps { generationStatus: RoadmapGenerationStatus; className?: string; + onStop?: () => void; } // Type for generation phases (excluding idle) @@ -190,6 +193,7 @@ function PhaseStepsIndicator({ export function RoadmapGenerationProgress({ generationStatus, className, + onStop }: RoadmapGenerationProgressProps) { const { phase, progress, message, error } = generationStatus; const reducedMotion = useReducedMotion(); @@ -248,6 +252,25 @@ export function RoadmapGenerationProgress({ return (
+ {/* Header with Stop button */} + {isActivePhase && onStop && ( +
+ + + + + Stop generation + +
+ )} + {/* Main phase display */}
{/* Animated icon with pulsing animation for active phase */} diff --git a/auto-claude-ui/src/renderer/components/roadmap/RoadmapHeader.tsx b/auto-claude-ui/src/renderer/components/roadmap/RoadmapHeader.tsx index 19868d7f..1c416260 100644 --- a/auto-claude-ui/src/renderer/components/roadmap/RoadmapHeader.tsx +++ b/auto-claude-ui/src/renderer/components/roadmap/RoadmapHeader.tsx @@ -1,4 +1,4 @@ -import { Target, Users, BarChart3, RefreshCw, Plus } from 'lucide-react'; +import { Target, Users, BarChart3, RefreshCw, Plus, TrendingUp } from 'lucide-react'; import { Badge } from '../ui/badge'; import { Button } from '../ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; @@ -6,7 +6,7 @@ import { getFeatureStats } from '../../stores/roadmap-store'; import { ROADMAP_PRIORITY_COLORS } from '../../../shared/constants'; import type { RoadmapHeaderProps } from './types'; -export function RoadmapHeader({ roadmap, onAddFeature, onRefresh }: RoadmapHeaderProps) { +export function RoadmapHeader({ roadmap, competitorAnalysis, onAddFeature, onRefresh }: RoadmapHeaderProps) { const stats = getFeatureStats(roadmap); return ( @@ -17,6 +17,27 @@ export function RoadmapHeader({ roadmap, onAddFeature, onRefresh }: RoadmapHeade

{roadmap.projectName}

{roadmap.status} + {competitorAnalysis && ( + + + + + Competitor Analysis + + + +
+
Analyzed {competitorAnalysis.competitors.length} competitors:
+ {competitorAnalysis.competitors.map((comp, idx) => ( +
+
• {comp.name}
+
{comp.painPoints.length} pain points identified
+
+ ))} +
+
+
+ )}

{roadmap.vision}

@@ -49,9 +70,21 @@ export function RoadmapHeader({ roadmap, onAddFeature, onRefresh }: RoadmapHeade {roadmap.targetAudience.primary}
{roadmap.targetAudience.secondary.length > 0 && ( -
- +{roadmap.targetAudience.secondary.length} more personas -
+ + +
+ +{roadmap.targetAudience.secondary.length} more personas +
+
+ +
+
Secondary Personas:
+ {roadmap.targetAudience.secondary.map((persona, idx) => ( +
• {persona}
+ ))} +
+
+
)} diff --git a/auto-claude-ui/src/renderer/components/roadmap/hooks.ts b/auto-claude-ui/src/renderer/components/roadmap/hooks.ts index e82e90c6..171c80f2 100644 --- a/auto-claude-ui/src/renderer/components/roadmap/hooks.ts +++ b/auto-claude-ui/src/renderer/components/roadmap/hooks.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useRoadmapStore, loadRoadmap, generateRoadmap, refreshRoadmap } from '../../stores/roadmap-store'; +import { useRoadmapStore, loadRoadmap, generateRoadmap, refreshRoadmap, stopRoadmap } from '../../stores/roadmap-store'; import type { RoadmapFeature } from '../../../shared/types'; /** @@ -70,22 +70,26 @@ export function useRoadmapGeneration(projectId: string) { const handleCompetitorDialogAccept = () => { if (pendingAction === 'generate') { - generateRoadmap(projectId); + generateRoadmap(projectId, true); // Enable competitor analysis } else if (pendingAction === 'refresh') { - refreshRoadmap(projectId); + refreshRoadmap(projectId, true); // Enable competitor analysis } setPendingAction(null); }; const handleCompetitorDialogDecline = () => { if (pendingAction === 'generate') { - generateRoadmap(projectId); + generateRoadmap(projectId, false); // Disable competitor analysis } else if (pendingAction === 'refresh') { - refreshRoadmap(projectId); + refreshRoadmap(projectId, false); // Disable competitor analysis } setPendingAction(null); }; + const handleStop = async () => { + await stopRoadmap(projectId); + }; + return { pendingAction, showCompetitorDialog, @@ -94,5 +98,6 @@ export function useRoadmapGeneration(projectId: string) { handleRefresh, handleCompetitorDialogAccept, handleCompetitorDialogDecline, + handleStop, }; } diff --git a/auto-claude-ui/src/renderer/components/roadmap/types.ts b/auto-claude-ui/src/renderer/components/roadmap/types.ts index fc46ac1f..797a6f7b 100644 --- a/auto-claude-ui/src/renderer/components/roadmap/types.ts +++ b/auto-claude-ui/src/renderer/components/roadmap/types.ts @@ -32,6 +32,7 @@ export interface FeatureDetailPanelProps { export interface RoadmapHeaderProps { roadmap: Roadmap; + competitorAnalysis: CompetitorAnalysis | null; onAddFeature: () => void; onRefresh: () => void; } diff --git a/auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx b/auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx index 88cec252..0c67ea5d 100644 --- a/auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx +++ b/auto-claude-ui/src/renderer/components/settings/AdvancedSettings.tsx @@ -171,7 +171,7 @@ export function AdvancedSettings({ settings, onSettingsChange, section, version setSourceUpdateCheck(result.data); } } catch (err) { - console.error('Failed to check for source updates:', err); + // Silent fail - user can retry via button } finally { setIsCheckingSourceUpdate(false); } diff --git a/auto-claude-ui/src/renderer/hooks/useIpc.ts b/auto-claude-ui/src/renderer/hooks/useIpc.ts index 5b19430b..ba1103a4 100644 --- a/auto-claude-ui/src/renderer/hooks/useIpc.ts +++ b/auto-claude-ui/src/renderer/hooks/useIpc.ts @@ -53,12 +53,29 @@ export function useIpcListeners(): void { const cleanupRoadmapProgress = window.electronAPI.onRoadmapProgress( (_projectId: string, status: RoadmapGenerationStatus) => { + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Progress update:', { + projectId: _projectId, + phase: status.phase, + progress: status.progress, + message: status.message + }); + } setGenerationStatus(status); } ); const cleanupRoadmapComplete = window.electronAPI.onRoadmapComplete( (_projectId: string, roadmap: Roadmap) => { + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Generation complete:', { + projectId: _projectId, + featuresCount: roadmap.features?.length || 0, + phasesCount: roadmap.phases?.length || 0 + }); + } setRoadmap(roadmap); setGenerationStatus({ phase: 'complete', @@ -70,6 +87,10 @@ export function useIpcListeners(): void { const cleanupRoadmapError = window.electronAPI.onRoadmapError( (_projectId: string, error: string) => { + // Debug logging + if (window.DEBUG) { + console.error('[Roadmap] Error received:', { projectId: _projectId, error }); + } setGenerationStatus({ phase: 'error', progress: 0, @@ -79,6 +100,20 @@ export function useIpcListeners(): void { } ); + const cleanupRoadmapStopped = window.electronAPI.onRoadmapStopped( + (_projectId: string) => { + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Generation stopped:', { projectId: _projectId }); + } + setGenerationStatus({ + phase: 'idle', + progress: 0, + message: 'Generation stopped' + }); + } + ); + // Terminal rate limit listener const showRateLimitModal = useRateLimitStore.getState().showRateLimitModal; const cleanupRateLimit = window.electronAPI.onTerminalRateLimit( @@ -117,6 +152,7 @@ export function useIpcListeners(): void { cleanupRoadmapProgress(); cleanupRoadmapComplete(); cleanupRoadmapError(); + cleanupRoadmapStopped(); cleanupRateLimit(); cleanupSDKRateLimit(); }; diff --git a/auto-claude-ui/src/renderer/stores/ideation-store.ts b/auto-claude-ui/src/renderer/stores/ideation-store.ts index da3962ca..44923b78 100644 --- a/auto-claude-ui/src/renderer/stores/ideation-store.ts +++ b/auto-claude-ui/src/renderer/stores/ideation-store.ts @@ -337,6 +337,18 @@ export async function loadIdeation(projectId: string): Promise { export function generateIdeation(projectId: string): void { const store = useIdeationStore.getState(); const config = store.config; + + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Starting generation:', { + projectId, + enabledTypes: config.enabledTypes, + includeRoadmapContext: config.includeRoadmapContext, + includeKanbanContext: config.includeKanbanContext, + maxIdeasPerType: config.maxIdeasPerType + }); + } + store.clearLogs(); store.clearSession(); // Clear existing session for fresh generation store.initializeTypeStates(config.enabledTypes); @@ -351,15 +363,35 @@ export function generateIdeation(projectId: string): void { export async function stopIdeation(projectId: string): Promise { const store = useIdeationStore.getState(); - const result = await window.electronAPI.stopIdeation(projectId); - if (result.success) { - store.addLog('Ideation generation stopped'); - store.setGenerationStatus({ - phase: 'idle', - progress: 0, - message: 'Generation stopped' - }); + + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Stop requested:', { projectId }); } + + // Always update UI state to 'idle' when user requests stop, regardless of backend response + // This prevents the UI from getting stuck in "generating" state if the process already ended + store.addLog('Stopping ideation generation...'); + store.setGenerationStatus({ + phase: 'idle', + progress: 0, + message: 'Generation stopped' + }); + + const result = await window.electronAPI.stopIdeation(projectId); + + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Stop result:', { projectId, success: result.success }); + } + + if (!result.success) { + // Backend couldn't find/stop the process (likely already finished/crashed) + store.addLog('Process already stopped'); + } else { + store.addLog('Ideation generation stopped'); + } + return result.success; } @@ -528,6 +560,15 @@ export function setupIdeationListeners(): () => void { // Listen for progress updates const unsubProgress = window.electronAPI.onIdeationProgress((_projectId, status) => { + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Progress update:', { + projectId: _projectId, + phase: status.phase, + progress: status.progress, + message: status.message + }); + } store().setGenerationStatus(status); }); @@ -539,6 +580,16 @@ export function setupIdeationListeners(): () => void { // Listen for individual ideation type completion (streaming) const unsubTypeComplete = window.electronAPI.onIdeationTypeComplete( (_projectId, ideationType, ideas) => { + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Type completed:', { + projectId: _projectId, + ideationType, + ideasCount: ideas.length, + ideas: ideas.map(i => ({ id: i.id, title: i.title, type: i.type })) + }); + } + store().addIdeasForType(ideationType, ideas); store().addLog(`✓ ${ideationType} completed with ${ideas.length} ideas`); @@ -564,6 +615,11 @@ export function setupIdeationListeners(): () => void { // Listen for individual ideation type failure const unsubTypeFailed = window.electronAPI.onIdeationTypeFailed( (_projectId, ideationType) => { + // Debug logging + if (window.DEBUG) { + console.error('[Ideation] Type failed:', { projectId: _projectId, ideationType }); + } + store().setTypeState(ideationType as IdeationType, 'failed'); store().addLog(`✗ ${ideationType} failed`); } @@ -571,6 +627,18 @@ export function setupIdeationListeners(): () => void { // Listen for completion (final session with all data) const unsubComplete = window.electronAPI.onIdeationComplete((_projectId, session) => { + // Debug logging + if (window.DEBUG) { + console.log('[Ideation] Generation complete:', { + projectId: _projectId, + totalIdeas: session.ideas.length, + ideaTypes: session.ideas.reduce((acc, idea) => { + acc[idea.type] = (acc[idea.type] || 0) + 1; + return acc; + }, {} as Record) + }); + } + // Final session replaces the partial one with complete data store().setSession(session); store().setGenerationStatus({ @@ -583,6 +651,11 @@ export function setupIdeationListeners(): () => void { // Listen for errors const unsubError = window.electronAPI.onIdeationError((_projectId, error) => { + // Debug logging + if (window.DEBUG) { + console.error('[Ideation] Error received:', { projectId: _projectId, error }); + } + store().setGenerationStatus({ phase: 'error', progress: 0, diff --git a/auto-claude-ui/src/renderer/stores/roadmap-store.ts b/auto-claude-ui/src/renderer/stores/roadmap-store.ts index 72528098..bed99ebc 100644 --- a/auto-claude-ui/src/renderer/stores/roadmap-store.ts +++ b/auto-claude-ui/src/renderer/stores/roadmap-store.ts @@ -176,22 +176,58 @@ export async function loadRoadmap(projectId: string): Promise { } } -export function generateRoadmap(projectId: string): void { +export function generateRoadmap(projectId: string, enableCompetitorAnalysis?: boolean): void { + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Starting generation:', { projectId, enableCompetitorAnalysis }); + } + useRoadmapStore.getState().setGenerationStatus({ phase: 'analyzing', progress: 0, message: 'Starting roadmap generation...' }); - window.electronAPI.generateRoadmap(projectId); + window.electronAPI.generateRoadmap(projectId, enableCompetitorAnalysis); } -export function refreshRoadmap(projectId: string): void { +export function refreshRoadmap(projectId: string, enableCompetitorAnalysis?: boolean): void { useRoadmapStore.getState().setGenerationStatus({ phase: 'analyzing', progress: 0, message: 'Refreshing roadmap...' }); - window.electronAPI.refreshRoadmap(projectId); + window.electronAPI.refreshRoadmap(projectId, enableCompetitorAnalysis); +} + +export async function stopRoadmap(projectId: string): Promise { + const store = useRoadmapStore.getState(); + + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Stop requested:', { projectId }); + } + + // Always update UI state to 'idle' when user requests stop, regardless of backend response + // This prevents the UI from getting stuck in "generating" state if the process already ended + store.setGenerationStatus({ + phase: 'idle', + progress: 0, + message: 'Generation stopped' + }); + + const result = await window.electronAPI.stopRoadmap(projectId); + + // Debug logging + if (window.DEBUG) { + console.log('[Roadmap] Stop result:', { projectId, success: result.success }); + } + + if (!result.success) { + // Backend couldn't find/stop the process (likely already finished/crashed) + console.log('[Roadmap] Process already stopped'); + } + + return result.success; } // Selectors diff --git a/auto-claude-ui/src/shared/constants/ipc.ts b/auto-claude-ui/src/shared/constants/ipc.ts index c121a8b6..f5ec0ab0 100644 --- a/auto-claude-ui/src/shared/constants/ipc.ts +++ b/auto-claude-ui/src/shared/constants/ipc.ts @@ -120,6 +120,7 @@ export const IPC_CHANNELS = { ROADMAP_GENERATE: 'roadmap:generate', ROADMAP_GENERATE_WITH_COMPETITOR: 'roadmap:generateWithCompetitor', ROADMAP_REFRESH: 'roadmap:refresh', + ROADMAP_STOP: 'roadmap:stop', ROADMAP_UPDATE_FEATURE: 'roadmap:updateFeature', ROADMAP_CONVERT_TO_SPEC: 'roadmap:convertToSpec', @@ -127,6 +128,7 @@ export const IPC_CHANNELS = { ROADMAP_PROGRESS: 'roadmap:progress', ROADMAP_COMPLETE: 'roadmap:complete', ROADMAP_ERROR: 'roadmap:error', + ROADMAP_STOPPED: 'roadmap:stopped', // Context operations CONTEXT_GET: 'context:get', diff --git a/auto-claude-ui/src/shared/types/ipc.ts b/auto-claude-ui/src/shared/types/ipc.ts index 0d86b2c8..47c13e68 100644 --- a/auto-claude-ui/src/shared/types/ipc.ts +++ b/auto-claude-ui/src/shared/types/ipc.ts @@ -238,7 +238,9 @@ export interface ElectronAPI { getRoadmap: (projectId: string) => Promise>; saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise; generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; - refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; updateFeatureStatus: ( + refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; + stopRoadmap: (projectId: string) => Promise; + updateFeatureStatus: ( projectId: string, featureId: string, status: RoadmapFeatureStatus @@ -258,6 +260,9 @@ export interface ElectronAPI { onRoadmapError: ( callback: (projectId: string, error: string) => void ) => () => void; + onRoadmapStopped: ( + callback: (projectId: string) => void + ) => () => void; // Context operations getProjectContext: (projectId: string) => Promise>; @@ -507,5 +512,6 @@ export interface ElectronAPI { declare global { interface Window { electronAPI: ElectronAPI; + DEBUG: boolean; } } diff --git a/auto-claude-ui/src/shared/types/task.ts b/auto-claude-ui/src/shared/types/task.ts index f27212cb..c4896278 100644 --- a/auto-claude-ui/src/shared/types/task.ts +++ b/auto-claude-ui/src/shared/types/task.ts @@ -239,9 +239,10 @@ export interface Task { // Implementation Plan (from auto-claude) export interface ImplementationPlan { - feature: string; + feature?: string; // Some plans use 'feature', some use 'title' + title?: string; // Alternative to 'feature' for task name workflow_type: string; - services_involved: string[]; + services_involved?: string[]; phases: Phase[]; final_acceptance: string[]; created_at: string; diff --git a/auto-claude/ideation/analyzer.py b/auto-claude/ideation/analyzer.py index e92885d5..f4012fea 100644 --- a/auto-claude/ideation/analyzer.py +++ b/auto-claude/ideation/analyzer.py @@ -30,13 +30,13 @@ class ProjectAnalyzer: self, project_dir: Path, output_dir: Path, - include_roadmap: bool = True, - include_kanban: bool = True, + include_roadmap_context: bool = True, + include_kanban_context: bool = True, ): self.project_dir = Path(project_dir) self.output_dir = Path(output_dir) - self.include_roadmap = include_roadmap - self.include_kanban = include_kanban + self.include_roadmap = include_roadmap_context + self.include_kanban = include_kanban_context def gather_context(self) -> dict: """Gather context from project for ideation.""" diff --git a/auto-claude/ideation/phase_executor.py b/auto-claude/ideation/phase_executor.py index 58993bee..991910bb 100644 --- a/auto-claude/ideation/phase_executor.py +++ b/auto-claude/ideation/phase_executor.py @@ -193,8 +193,8 @@ class PhaseExecutor: "graph_hints": graph_hints, # Include graph hints in context "config": { "enabled_types": self.enabled_types, - "include_roadmap_context": self.analyzer.include_roadmap_context, - "include_kanban_context": self.analyzer.include_kanban_context, + "include_roadmap_context": self.analyzer.include_roadmap, + "include_kanban_context": self.analyzer.include_kanban, "max_ideas_per_type": self.max_ideas_per_type, }, "created_at": datetime.now().isoformat(), diff --git a/auto-claude/implementation_plan/plan.py b/auto-claude/implementation_plan/plan.py index 46ef62eb..13e1c735 100644 --- a/auto-claude/implementation_plan/plan.py +++ b/auto-claude/implementation_plan/plan.py @@ -77,8 +77,11 @@ class ImplementationPlan: ) workflow_type = WorkflowType.FEATURE + # Support both 'feature' and 'title' fields for task name + feature_name = data.get("feature") or data.get("title") or "Unnamed Feature" + return cls( - feature=data["feature"], + feature=feature_name, workflow_type=workflow_type, services_involved=data.get("services_involved", []), phases=[ diff --git a/auto-claude/prompts/planner.md b/auto-claude/prompts/planner.md index 5bc82fb5..9a27c670 100644 --- a/auto-claude/prompts/planner.md +++ b/auto-claude/prompts/planner.md @@ -209,6 +209,7 @@ Based on the workflow type and services involved, create the implementation plan ```json { + "feature": "Short descriptive name for this task/feature", "workflow_type": "feature|refactor|investigation|migration|simple", "workflow_rationale": "Why this workflow type was chosen", "phases": [