feat: enhance roadmap generation with stop functionality and debug logging
- 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)
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<IPCResult> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IPCResult> => {
|
||||
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)
|
||||
// ============================================
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,12 +11,12 @@ import { TIMEOUTS } from './config';
|
||||
*/
|
||||
export function fetchJson<T>(url: string): Promise<T> {
|
||||
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<T>(url: string): Promise<T> {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface RoadmapAPI {
|
||||
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
|
||||
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
stopRoadmap: (projectId: string) => Promise<IPCResult>;
|
||||
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<IPCResult> =>
|
||||
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)
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [sourcePath, setSourcePath] = useState<string | null>(null);
|
||||
const [hasExistingToken, setHasExistingToken] = useState(false);
|
||||
const [claudeProfiles, setClaudeProfiles] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
oauthToken?: string;
|
||||
email?: string;
|
||||
isDefault: boolean;
|
||||
}>>([]);
|
||||
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(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({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info about getting a token */}
|
||||
<div className="rounded-lg bg-info/10 border border-info/30 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="h-5 w-5 text-info shrink-0 mt-0.5" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="text-sm text-foreground font-medium">
|
||||
How to get a Claude Code OAuth token:
|
||||
</p>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Install Claude Code CLI if you haven't already</li>
|
||||
<li>
|
||||
Run{' '}
|
||||
<code className="px-1.5 py-0.5 bg-muted rounded font-mono text-xs">
|
||||
claude setup-token
|
||||
</code>
|
||||
{' '}
|
||||
{/* Option 1: Use existing authenticated profile */}
|
||||
{!isLoadingProfiles && claudeProfiles.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg bg-success/10 border border-success/30 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="h-5 w-5 text-success shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-foreground font-medium mb-1">
|
||||
Use Existing Account
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
You have {claudeProfiles.length} authenticated Claude account{claudeProfiles.length > 1 ? 's' : ''}. Select one to use:
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile selector */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">
|
||||
Select Account
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{claudeProfiles.map((profile) => (
|
||||
<button
|
||||
onClick={handleCopyCommand}
|
||||
className="inline-flex items-center text-info hover:text-info/80"
|
||||
key={profile.id}
|
||||
onClick={() => setSelectedProfileId(profile.id)}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 p-3 rounded-lg border-2 transition-colors text-left",
|
||||
selectedProfileId === profile.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
)}
|
||||
>
|
||||
<Copy className="h-3 w-3 ml-1" />
|
||||
<div className={cn(
|
||||
"h-4 w-4 rounded-full border-2 flex items-center justify-center shrink-0",
|
||||
selectedProfileId === profile.id
|
||||
? "border-primary"
|
||||
: "border-muted-foreground"
|
||||
)}>
|
||||
{selectedProfileId === profile.id && (
|
||||
<div className="h-2 w-2 rounded-full bg-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{profile.name}
|
||||
{profile.isDefault && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
|
||||
)}
|
||||
</p>
|
||||
{profile.email && (
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{profile.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<CheckCircle2 className={cn(
|
||||
"h-4 w-4 shrink-0",
|
||||
selectedProfileId === profile.id ? "text-primary" : "text-transparent"
|
||||
)} />
|
||||
</button>
|
||||
</li>
|
||||
<li>Copy the token and paste it below</li>
|
||||
</ol>
|
||||
<button
|
||||
onClick={handleOpenDocs}
|
||||
className="text-sm text-info hover:text-info/80 flex items-center gap-1"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
View documentation
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleUseExistingProfile}
|
||||
disabled={!selectedProfileId || isSaving}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Key className="mr-2 h-5 w-5" />
|
||||
Use This Account
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">or</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Token input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="token" className="text-sm font-medium text-foreground">
|
||||
Claude Code OAuth Token
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="token"
|
||||
type={showToken ? 'text' : 'password'}
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="Enter your token..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showToken ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showToken ? 'Hide token' : 'Show token'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{/* Option 2: Authenticate new account with browser */}
|
||||
{!isLoadingProfiles && (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg bg-info/10 border border-info/30 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="h-5 w-5 text-info shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-foreground font-medium mb-1">
|
||||
{claudeProfiles.length > 0 ? 'Or Authenticate New Account' : 'Authenticate with Browser'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleAuthenticateWithBrowser}
|
||||
disabled={isAuthenticating}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
variant={claudeProfiles.length > 0 ? "outline" : "default"}
|
||||
>
|
||||
{isAuthenticating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Waiting for authentication...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogIn className="mr-2 h-5 w-5" />
|
||||
{claudeProfiles.length > 0 ? 'Authenticate New Account' : 'Authenticate with Browser'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isAuthenticating && (
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
A browser window should open. Complete the authentication there, then return here.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The token will be saved to{' '}
|
||||
<code className="px-1 py-0.5 bg-muted rounded font-mono">
|
||||
{sourcePath ? `${sourcePath}/.env` : 'auto-claude/.env'}
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Divider before manual entry */}
|
||||
{!isLoadingProfiles && (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<div className="w-full border-t border-border"></div>
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">or</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Secondary: Manual Token Entry (Collapsible) */}
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => setShowManualEntry(!showManualEntry)}
|
||||
className="w-full flex items-center justify-between text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<span>Enter token manually</span>
|
||||
{showManualEntry ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showManualEntry && (
|
||||
<div className="space-y-3 pl-4 border-l-2 border-border">
|
||||
{/* Manual token instructions */}
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p className="font-medium text-foreground">Steps:</p>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>Install Claude Code CLI if you haven't already</li>
|
||||
<li>
|
||||
Run{' '}
|
||||
<code className="px-1 py-0.5 bg-muted rounded font-mono">
|
||||
claude setup-token
|
||||
</code>
|
||||
{' '}
|
||||
<button
|
||||
onClick={handleCopyCommand}
|
||||
className="inline-flex items-center text-info hover:text-info/80"
|
||||
>
|
||||
<Copy className="h-3 w-3 ml-1" />
|
||||
</button>
|
||||
</li>
|
||||
<li>Copy the token and paste it below</li>
|
||||
</ol>
|
||||
<button
|
||||
onClick={handleOpenDocs}
|
||||
className="text-info hover:text-info/80 flex items-center gap-1 mt-2"
|
||||
>
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
View documentation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Token input */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="token" className="text-sm font-medium text-foreground">
|
||||
Claude Code OAuth Token
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="token"
|
||||
type={showToken ? 'text' : 'password'}
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="Enter your token..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving || isAuthenticating}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowToken(!showToken)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showToken ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showToken ? 'Hide token' : 'Show token'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
The token will be saved to{' '}
|
||||
<code className="px-1 py-0.5 bg-muted rounded font-mono">
|
||||
{sourcePath ? `${sourcePath}/.env` : 'auto-claude/.env'}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Existing token info */}
|
||||
{hasExistingToken && (
|
||||
<div className="rounded-lg bg-muted/50 p-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -272,11 +566,11 @@ export function EnvConfigModal({
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isSaving}>
|
||||
<Button variant="outline" onClick={handleClose} disabled={isSaving || isAuthenticating}>
|
||||
{success ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
{!success && (
|
||||
<Button onClick={handleSave} disabled={!token.trim() || isSaving}>
|
||||
{!success && showManualEntry && token.trim() && (
|
||||
<Button onClick={handleSave} disabled={isSaving || isAuthenticating}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
|
||||
@@ -27,6 +27,7 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) {
|
||||
handleRefresh,
|
||||
handleCompetitorDialogAccept,
|
||||
handleCompetitorDialogDecline,
|
||||
handleStop,
|
||||
} = useRoadmapGeneration(projectId);
|
||||
|
||||
// Event handlers
|
||||
@@ -47,6 +48,7 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) {
|
||||
<RoadmapGenerationProgress
|
||||
generationStatus={generationStatus}
|
||||
className="w-full max-w-md"
|
||||
onStop={handleStop}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -73,6 +75,7 @@ export function Roadmap({ projectId, onGoToTask }: RoadmapProps) {
|
||||
{/* Header */}
|
||||
<RoadmapHeader
|
||||
roadmap={roadmap}
|
||||
competitorAnalysis={competitorAnalysis}
|
||||
onAddFeature={() => setShowAddFeatureDialog(true)}
|
||||
onRefresh={handleRefresh}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<div className={cn('space-y-4 p-6 rounded-xl bg-card border', className)}>
|
||||
{/* Header with Stop button */}
|
||||
{isActivePhase && onStop && (
|
||||
<div className="flex justify-end mb-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onStop}
|
||||
>
|
||||
<Square className="h-4 w-4 mr-1" />
|
||||
Stop
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop generation</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main phase display */}
|
||||
<div className="flex flex-col items-center text-center space-y-3">
|
||||
{/* Animated icon with pulsing animation for active phase */}
|
||||
|
||||
@@ -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
|
||||
<Target className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">{roadmap.projectName}</h2>
|
||||
<Badge variant="outline">{roadmap.status}</Badge>
|
||||
{competitorAnalysis && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
Competitor Analysis
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md">
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold">Analyzed {competitorAnalysis.competitors.length} competitors:</div>
|
||||
{competitorAnalysis.competitors.map((comp, idx) => (
|
||||
<div key={idx} className="text-sm">
|
||||
<div className="font-medium">• {comp.name}</div>
|
||||
<div className="text-muted-foreground ml-3">{comp.painPoints.length} pain points identified</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground max-w-xl">{roadmap.vision}</p>
|
||||
</div>
|
||||
@@ -49,9 +70,21 @@ export function RoadmapHeader({ roadmap, onAddFeature, onRefresh }: RoadmapHeade
|
||||
<span className="font-medium">{roadmap.targetAudience.primary}</span>
|
||||
</div>
|
||||
{roadmap.targetAudience.secondary.length > 0 && (
|
||||
<div className="text-muted-foreground">
|
||||
+{roadmap.targetAudience.secondary.length} more personas
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="text-muted-foreground cursor-help underline decoration-dotted">
|
||||
+{roadmap.targetAudience.secondary.length} more personas
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-md">
|
||||
<div className="space-y-1">
|
||||
<div className="font-semibold mb-2">Secondary Personas:</div>
|
||||
{roadmap.targetAudience.secondary.map((persona, idx) => (
|
||||
<div key={idx} className="text-sm">• {persona}</div>
|
||||
))}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface FeatureDetailPanelProps {
|
||||
|
||||
export interface RoadmapHeaderProps {
|
||||
roadmap: Roadmap;
|
||||
competitorAnalysis: CompetitorAnalysis | null;
|
||||
onAddFeature: () => void;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -337,6 +337,18 @@ export async function loadIdeation(projectId: string): Promise<void> {
|
||||
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<boolean> {
|
||||
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<string, number>)
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -176,22 +176,58 @@ export async function loadRoadmap(projectId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
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<boolean> {
|
||||
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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -238,7 +238,9 @@ export interface ElectronAPI {
|
||||
getRoadmap: (projectId: string) => Promise<IPCResult<Roadmap | null>>;
|
||||
saveRoadmap: (projectId: string, roadmap: Roadmap) => Promise<IPCResult>;
|
||||
generateRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void; updateFeatureStatus: (
|
||||
refreshRoadmap: (projectId: string, enableCompetitorAnalysis?: boolean) => void;
|
||||
stopRoadmap: (projectId: string) => Promise<IPCResult>;
|
||||
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<IPCResult<ProjectContextData>>;
|
||||
@@ -507,5 +512,6 @@ export interface ElectronAPI {
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
DEBUG: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user