diff --git a/auto-claude-ui/package.json b/auto-claude-ui/package.json index 4653a3f4..cfd8a652 100644 --- a/auto-claude-ui/package.json +++ b/auto-claude-ui/package.json @@ -15,6 +15,7 @@ "package:mac": "electron-vite build && electron-builder --mac", "package:win": "electron-vite build && electron-builder --win", "package:linux": "electron-vite build && electron-builder --linux", + "start:packaged": "open dist/mac-arm64/Auto\\ Claude.app || open dist/mac/Auto\\ Claude.app", "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", diff --git a/auto-claude-ui/src/main/agent-manager.ts b/auto-claude-ui/src/main/agent-manager.ts index bc3b3dba..28f15921 100644 --- a/auto-claude-ui/src/main/agent-manager.ts +++ b/auto-claude-ui/src/main/agent-manager.ts @@ -73,11 +73,14 @@ export class AgentManager extends EventEmitter { private loadAutoBuildEnv(): Record { const autoBuildSource = this.getAutoBuildSourcePath(); if (!autoBuildSource) { + console.log('[loadAutoBuildEnv] No auto-build source path found'); return {}; } const envPath = path.join(autoBuildSource, '.env'); + console.log('[loadAutoBuildEnv] Looking for .env at:', envPath); if (!existsSync(envPath)) { + console.log('[loadAutoBuildEnv] .env file does not exist'); return {}; } @@ -230,17 +233,17 @@ export class AgentManager extends EventEmitter { const args = [ideationRunnerPath, '--project', projectPath]; - // Add enabled types + // Add enabled types as comma-separated list if (config.enabledTypes.length > 0) { - args.push('--types', ...config.enabledTypes); + args.push('--types', config.enabledTypes.join(',')); } - // Add context flags - if (config.includeRoadmapContext) { - args.push('--include-roadmap'); + // Add context flags (script uses --no-roadmap/--no-kanban negative flags) + if (!config.includeRoadmapContext) { + args.push('--no-roadmap'); } - if (config.includeKanbanContext) { - args.push('--include-kanban'); + if (!config.includeKanbanContext) { + args.push('--no-kanban'); } // Add max ideas per type @@ -299,11 +302,18 @@ export class AgentManager extends EventEmitter { for (const line of lines) { const trimmed = line.trim(); if (trimmed.length > 0) { + console.log('[Ideation]', trimmed); this.emit('ideation-log', projectId, trimmed); } } }; + console.log('[Ideation] Starting ideation process with args:', args); + console.log('[Ideation] CWD:', cwd); + console.log('[Ideation] Python path:', this.pythonPath); + console.log('[Ideation] Env vars loaded:', Object.keys(autoBuildEnv)); + console.log('[Ideation] Has CLAUDE_CODE_OAUTH_TOKEN:', !!autoBuildEnv['CLAUDE_CODE_OAUTH_TOKEN']); + // Handle stdout childProcess.stdout?.on('data', (data: Buffer) => { const log = data.toString(); @@ -356,6 +366,7 @@ export class AgentManager extends EventEmitter { // Handle stderr - also emit as logs childProcess.stderr?.on('data', (data: Buffer) => { const log = data.toString(); + console.error('[Ideation STDERR]', log); emitLogs(log); this.emit('ideation-progress', projectId, { phase: progressPhase, @@ -366,6 +377,7 @@ export class AgentManager extends EventEmitter { // Handle process exit childProcess.on('exit', (code: number | null) => { + console.log('[Ideation] Process exited with code:', code); this.processes.delete(projectId); if (code === 0) { @@ -381,6 +393,7 @@ export class AgentManager extends EventEmitter { // Handle process error childProcess.on('error', (err: Error) => { + console.error('[Ideation] Process error:', err.message); this.processes.delete(projectId); this.emit('ideation-error', projectId, err.message); }); diff --git a/auto-claude-ui/src/main/ipc-handlers.ts b/auto-claude-ui/src/main/ipc-handlers.ts index 3c5c040e..7cb5c367 100644 --- a/auto-claude-ui/src/main/ipc-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers.ts @@ -40,7 +40,9 @@ import type { IdeationSession, IdeationConfig, IdeationGenerationStatus, - IdeationStatus + IdeationStatus, + SourceEnvConfig, + SourceEnvCheckResult } from '../shared/types'; import { projectStore } from './project-store'; import { fileWatcher } from './file-watcher'; @@ -494,16 +496,26 @@ export function setupIpcHandlers( ipcMain.handle( IPC_CHANNELS.SETTINGS_GET, async (): Promise> => { + let settings = { ...DEFAULT_APP_SETTINGS }; + if (existsSync(settingsPath)) { try { const content = readFileSync(settingsPath, 'utf-8'); - const settings = JSON.parse(content); - return { success: true, data: { ...DEFAULT_APP_SETTINGS, ...settings } }; + settings = { ...settings, ...JSON.parse(content) }; } catch { - return { success: true, data: DEFAULT_APP_SETTINGS as AppSettings }; + // Use defaults } } - return { success: true, data: DEFAULT_APP_SETTINGS as AppSettings }; + + // If no manual autoBuildPath is set, try to auto-detect + if (!settings.autoBuildPath) { + const detectedPath = detectAutoBuildSourcePath(); + if (detectedPath) { + settings.autoBuildPath = detectedPath; + } + } + + return { success: true, data: settings as AppSettings }; } ); @@ -1479,89 +1491,111 @@ ${existingVars['GRAPHITI_DATABASE'] ? `GRAPHITI_DATABASE=${existingVars['GRAPHIT const envPath = path.join(project.path, project.autoBuildPath, '.env'); + // Load global settings for fallbacks + let globalSettings: AppSettings = { ...DEFAULT_APP_SETTINGS }; + if (existsSync(settingsPath)) { + try { + const content = readFileSync(settingsPath, 'utf-8'); + globalSettings = { ...globalSettings, ...JSON.parse(content) }; + } catch { + // Use defaults + } + } + // Default config const config: ProjectEnvConfig = { claudeAuthStatus: 'not_configured', linearEnabled: false, githubEnabled: false, graphitiEnabled: false, - enableFancyUi: true + enableFancyUi: true, + claudeTokenIsGlobal: false, + openaiKeyIsGlobal: false }; - if (!existsSync(envPath)) { - return { success: true, data: config }; + // Parse project-specific .env if it exists + let vars: Record = {}; + if (existsSync(envPath)) { + try { + const content = readFileSync(envPath, 'utf-8'); + vars = parseEnvFile(content); + } catch { + // Continue with empty vars + } } - try { - const content = readFileSync(envPath, 'utf-8'); - const vars = parseEnvFile(content); - - // Parse values into config - if (vars['CLAUDE_CODE_OAUTH_TOKEN']) { - config.claudeOAuthToken = vars['CLAUDE_CODE_OAUTH_TOKEN']; - config.claudeAuthStatus = 'token_set'; - } - - if (vars['AUTO_BUILD_MODEL']) { - config.autoBuildModel = vars['AUTO_BUILD_MODEL']; - } - - if (vars['LINEAR_API_KEY']) { - config.linearEnabled = true; - config.linearApiKey = vars['LINEAR_API_KEY']; - } - if (vars['LINEAR_TEAM_ID']) { - config.linearTeamId = vars['LINEAR_TEAM_ID']; - } - if (vars['LINEAR_PROJECT_ID']) { - config.linearProjectId = vars['LINEAR_PROJECT_ID']; - } - if (vars['LINEAR_REALTIME_SYNC']?.toLowerCase() === 'true') { - config.linearRealtimeSync = true; - } - - // GitHub config - if (vars['GITHUB_TOKEN']) { - config.githubEnabled = true; - config.githubToken = vars['GITHUB_TOKEN']; - } - if (vars['GITHUB_REPO']) { - config.githubRepo = vars['GITHUB_REPO']; - } - if (vars['GITHUB_AUTO_SYNC']?.toLowerCase() === 'true') { - config.githubAutoSync = true; - } - - if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') { - config.graphitiEnabled = true; - } - if (vars['OPENAI_API_KEY']) { - config.openaiApiKey = vars['OPENAI_API_KEY']; - } - if (vars['GRAPHITI_FALKORDB_HOST']) { - config.graphitiFalkorDbHost = vars['GRAPHITI_FALKORDB_HOST']; - } - if (vars['GRAPHITI_FALKORDB_PORT']) { - config.graphitiFalkorDbPort = parseInt(vars['GRAPHITI_FALKORDB_PORT'], 10); - } - if (vars['GRAPHITI_FALKORDB_PASSWORD']) { - config.graphitiFalkorDbPassword = vars['GRAPHITI_FALKORDB_PASSWORD']; - } - if (vars['GRAPHITI_DATABASE']) { - config.graphitiDatabase = vars['GRAPHITI_DATABASE']; - } - - if (vars['ENABLE_FANCY_UI']?.toLowerCase() === 'false') { - config.enableFancyUi = false; - } - - return { success: true, data: config }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to read .env file' - }; + // Claude OAuth Token: project-specific takes precedence, then global + if (vars['CLAUDE_CODE_OAUTH_TOKEN']) { + config.claudeOAuthToken = vars['CLAUDE_CODE_OAUTH_TOKEN']; + config.claudeAuthStatus = 'token_set'; + config.claudeTokenIsGlobal = false; + } else if (globalSettings.globalClaudeOAuthToken) { + config.claudeOAuthToken = globalSettings.globalClaudeOAuthToken; + config.claudeAuthStatus = 'token_set'; + config.claudeTokenIsGlobal = true; } + + if (vars['AUTO_BUILD_MODEL']) { + config.autoBuildModel = vars['AUTO_BUILD_MODEL']; + } + + if (vars['LINEAR_API_KEY']) { + config.linearEnabled = true; + config.linearApiKey = vars['LINEAR_API_KEY']; + } + if (vars['LINEAR_TEAM_ID']) { + config.linearTeamId = vars['LINEAR_TEAM_ID']; + } + if (vars['LINEAR_PROJECT_ID']) { + config.linearProjectId = vars['LINEAR_PROJECT_ID']; + } + if (vars['LINEAR_REALTIME_SYNC']?.toLowerCase() === 'true') { + config.linearRealtimeSync = true; + } + + // GitHub config + if (vars['GITHUB_TOKEN']) { + config.githubEnabled = true; + config.githubToken = vars['GITHUB_TOKEN']; + } + if (vars['GITHUB_REPO']) { + config.githubRepo = vars['GITHUB_REPO']; + } + if (vars['GITHUB_AUTO_SYNC']?.toLowerCase() === 'true') { + config.githubAutoSync = true; + } + + if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') { + config.graphitiEnabled = true; + } + + // OpenAI API Key: project-specific takes precedence, then global + if (vars['OPENAI_API_KEY']) { + config.openaiApiKey = vars['OPENAI_API_KEY']; + config.openaiKeyIsGlobal = false; + } else if (globalSettings.globalOpenAIApiKey) { + config.openaiApiKey = globalSettings.globalOpenAIApiKey; + config.openaiKeyIsGlobal = true; + } + + if (vars['GRAPHITI_FALKORDB_HOST']) { + config.graphitiFalkorDbHost = vars['GRAPHITI_FALKORDB_HOST']; + } + if (vars['GRAPHITI_FALKORDB_PORT']) { + config.graphitiFalkorDbPort = parseInt(vars['GRAPHITI_FALKORDB_PORT'], 10); + } + if (vars['GRAPHITI_FALKORDB_PASSWORD']) { + config.graphitiFalkorDbPassword = vars['GRAPHITI_FALKORDB_PASSWORD']; + } + if (vars['GRAPHITI_DATABASE']) { + config.graphitiDatabase = vars['GRAPHITI_DATABASE']; + } + + if (vars['ENABLE_FANCY_UI']?.toLowerCase() === 'false') { + config.enableFancyUi = false; + } + + return { success: true, data: config }; } ); @@ -2791,6 +2825,207 @@ ${issue.body || 'No description provided.'} } ); + // ============================================ + // Auto Claude Source Environment Operations + // ============================================ + + /** + * Parse an .env file content into a key-value object + */ + const parseSourceEnvFile = (content: string): Record => { + const vars: Record = {}; + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + const key = trimmed.substring(0, eqIndex).trim(); + let value = trimmed.substring(eqIndex + 1).trim(); + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + vars[key] = value; + } + } + return vars; + }; + + ipcMain.handle( + IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET, + async (): Promise> => { + try { + const sourcePath = getEffectiveSourcePath(); + if (!sourcePath) { + return { + success: true, + data: { + hasClaudeToken: false, + envExists: false, + sourcePath: undefined + } + }; + } + + const envPath = path.join(sourcePath, '.env'); + const envExists = existsSync(envPath); + + if (!envExists) { + return { + success: true, + data: { + hasClaudeToken: false, + envExists: false, + sourcePath + } + }; + } + + const content = readFileSync(envPath, 'utf-8'); + const vars = parseSourceEnvFile(content); + const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN']; + + return { + success: true, + data: { + hasClaudeToken: hasToken, + claudeOAuthToken: hasToken ? vars['CLAUDE_CODE_OAUTH_TOKEN'] : undefined, + envExists: true, + sourcePath + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to get source env' + }; + } + } + ); + + ipcMain.handle( + IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE, + async (_, config: { claudeOAuthToken?: string }): Promise => { + try { + const sourcePath = getEffectiveSourcePath(); + if (!sourcePath) { + return { + success: false, + error: 'Auto-Claude source path not found. Please configure it in App Settings.' + }; + } + + const envPath = path.join(sourcePath, '.env'); + + // Read existing content or start fresh + let existingContent = ''; + const existingVars: Record = {}; + + if (existsSync(envPath)) { + existingContent = readFileSync(envPath, 'utf-8'); + Object.assign(existingVars, parseSourceEnvFile(existingContent)); + } + + // Update the token + if (config.claudeOAuthToken !== undefined) { + existingVars['CLAUDE_CODE_OAUTH_TOKEN'] = config.claudeOAuthToken; + } + + // Rebuild the .env file preserving comments and structure + const lines = existingContent.split('\n'); + const processedKeys = new Set(); + const outputLines: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + outputLines.push(line); + continue; + } + + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + const key = trimmed.substring(0, eqIndex).trim(); + if (key in existingVars) { + outputLines.push(`${key}=${existingVars[key]}`); + processedKeys.add(key); + } else { + outputLines.push(line); + } + } else { + outputLines.push(line); + } + } + + // Add any new keys that weren't in the original file + for (const [key, value] of Object.entries(existingVars)) { + if (!processedKeys.has(key)) { + outputLines.push(`${key}=${value}`); + } + } + + writeFileSync(envPath, outputLines.join('\n')); + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update source env' + }; + } + } + ); + + ipcMain.handle( + IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN, + async (): Promise> => { + try { + const sourcePath = getEffectiveSourcePath(); + if (!sourcePath) { + return { + success: true, + data: { + hasToken: false, + sourcePath: undefined, + error: 'Auto-Claude source path not found' + } + }; + } + + const envPath = path.join(sourcePath, '.env'); + if (!existsSync(envPath)) { + return { + success: true, + data: { + hasToken: false, + sourcePath, + error: '.env file does not exist' + } + }; + } + + const content = readFileSync(envPath, 'utf-8'); + const vars = parseSourceEnvFile(content); + const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'] && vars['CLAUDE_CODE_OAUTH_TOKEN'].length > 0; + + return { + success: true, + data: { + hasToken, + sourcePath + } + }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to check source token' + }; + } + } + ); + // ============================================ // Ideation Operations // ============================================ diff --git a/auto-claude-ui/src/preload/index.ts b/auto-claude-ui/src/preload/index.ts index 0bfd94b0..4a2201ed 100644 --- a/auto-claude-ui/src/preload/index.ts +++ b/auto-claude-ui/src/preload/index.ts @@ -40,6 +40,8 @@ import type { IdeationGenerationStatus, AutoBuildSourceUpdateCheck, AutoBuildSourceUpdateProgress, + SourceEnvConfig, + SourceEnvCheckResult, ChangelogTask, TaskSpecContent, ChangelogGenerationRequest, @@ -595,6 +597,19 @@ const electronAPI: ElectronAPI = { }; }, + // ============================================ + // Auto-Build Source Environment Operations + // ============================================ + + getSourceEnv: (): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET), + + updateSourceEnv: (config: { claudeOAuthToken?: string }): Promise => + ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE, config), + + checkSourceToken: (): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN), + // ============================================ // Changelog Operations // ============================================ diff --git a/auto-claude-ui/src/renderer/App.tsx b/auto-claude-ui/src/renderer/App.tsx index b43e059a..6242080a 100644 --- a/auto-claude-ui/src/renderer/App.tsx +++ b/auto-claude-ui/src/renderer/App.tsx @@ -295,6 +295,65 @@ export function App() { onOpenChange={setIsProjectSettingsOpen} /> )} + + {/* Initialize Auto Claude Dialog */} + + + + + + Initialize Auto Claude + + + This project doesn't have Auto Claude initialized. Would you like to set it up now? + + +
+
+

This will:

+
    +
  • Create a .auto-claude folder in your project
  • +
  • Copy the Auto Claude framework files
  • +
  • Set up the specs directory for your tasks
  • +
+
+ {!settings.autoBuildPath && ( +
+
+ +
+

Source path not configured

+

+ Please set the Auto Claude source path in App Settings before initializing. +

+
+
+
+ )} +
+ + + + +
+
); diff --git a/auto-claude-ui/src/renderer/components/AppSettings.tsx b/auto-claude-ui/src/renderer/components/AppSettings.tsx index 10f26645..aa55c59c 100644 --- a/auto-claude-ui/src/renderer/components/AppSettings.tsx +++ b/auto-claude-ui/src/renderer/components/AppSettings.tsx @@ -10,7 +10,11 @@ import { RefreshCw, CheckCircle2, AlertCircle, - CloudDownload + CloudDownload, + Key, + Eye, + EyeOff, + Info } from 'lucide-react'; import { Dialog, @@ -60,6 +64,10 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps const [isDownloadingUpdate, setIsDownloadingUpdate] = useState(false); const [downloadProgress, setDownloadProgress] = useState(null); + // Password visibility toggles for global API keys + const [showGlobalClaudeToken, setShowGlobalClaudeToken] = useState(false); + const [showGlobalOpenAIKey, setShowGlobalOpenAIKey] = useState(false); + // Load settings on mount useEffect(() => { loadSettings(); @@ -298,6 +306,78 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps + {/* Global API Keys */} +
+
+ +

Global API Keys

+
+
+
+ +

+ Set API keys here to use them across all projects. Individual projects can override these in their settings. +

+
+
+
+ +
+ + setSettings({ ...settings, globalClaudeOAuthToken: e.target.value || undefined }) + } + className="pr-10 font-mono text-sm" + /> + +
+

+ Get your token by running claude setup-token +

+
+
+ +
+ + setSettings({ ...settings, globalOpenAIApiKey: e.target.value || undefined }) + } + className="pr-10 font-mono text-sm" + /> + +
+

+ Required for Graphiti memory backend (embeddings) +

+
+
+ + + {/* Auto Claude Framework Updates */}

Auto Claude Framework

diff --git a/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx b/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx new file mode 100644 index 00000000..84f2f705 --- /dev/null +++ b/auto-claude-ui/src/renderer/components/EnvConfigModal.tsx @@ -0,0 +1,333 @@ +import { useState, useEffect } from 'react'; +import { + AlertCircle, + Key, + Loader2, + CheckCircle2, + ExternalLink, + Copy, + Eye, + EyeOff, + Info +} from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from './ui/tooltip'; + +interface EnvConfigModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfigured?: () => void; + title?: string; + description?: string; +} + +export function EnvConfigModal({ + open, + onOpenChange, + onConfigured, + title = 'Claude Authentication Required', + description = 'A Claude Code OAuth token is required to use AI features like Ideation and Roadmap generation.' +}: EnvConfigModalProps) { + const [token, setToken] = useState(''); + const [showToken, setShowToken] = useState(false); + const [isLoading, setIsLoading] = 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); + + // Check current token status when modal opens + useEffect(() => { + const checkToken = async () => { + if (!open) return; + + setIsChecking(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); + + if (result.data.hasToken) { + // Token exists, show success state + setSuccess(true); + } + } else { + setError(result.error || 'Failed to check token status'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsChecking(false); + } + }; + + checkToken(); + }, [open]); + + const handleSave = async () => { + if (!token.trim()) { + setError('Please enter a token'); + return; + } + + setIsSaving(true); + setError(null); + + try { + const result = await window.electronAPI.updateSourceEnv({ + claudeOAuthToken: token.trim() + }); + + if (result.success) { + setSuccess(true); + setHasExistingToken(true); + setToken(''); // Clear the input + + // Notify parent that configuration is complete + 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 handleCopyCommand = () => { + navigator.clipboard.writeText('claude setup-token'); + }; + + const handleOpenDocs = () => { + // Open the Claude Code documentation for getting a token + window.open('https://docs.anthropic.com/en/docs/claude-code', '_blank'); + }; + + const handleClose = () => { + if (!isSaving) { + setToken(''); + setError(null); + setSuccess(false); + onOpenChange(false); + } + }; + + return ( + + + + + + {title} + + {description} + + + {/* Loading state */} + {isChecking && ( +
+ +
+ )} + + {/* Success state */} + {!isChecking && success && ( +
+
+ +
+

+ Token configured successfully +

+

+ You can now use AI features like Ideation and Roadmap generation. +

+
+
+
+ )} + + {/* Configuration form */} + {!isChecking && !success && ( +
+ {/* Error banner */} + {error && ( +
+ +

{error}

+
+ )} + + {/* 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 + + {' '} + +
  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} + /> + + + + + + {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. +

+
+ )} +
+ )} + + + + {!success && ( + + )} + +
+
+ ); +} + +/** + * Hook to check if the Claude token is configured + * Returns { hasToken, isLoading, checkToken } + */ +export function useClaudeTokenCheck() { + const [hasToken, setHasToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const checkToken = async () => { + setIsLoading(true); + setError(null); + + try { + const result = await window.electronAPI.checkSourceToken(); + if (result.success && result.data) { + setHasToken(result.data.hasToken); + } else { + setHasToken(false); + setError(result.error || 'Failed to check token'); + } + } catch (err) { + setHasToken(false); + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + checkToken(); + }, []); + + return { hasToken, isLoading, error, checkToken }; +} diff --git a/auto-claude-ui/src/renderer/components/Ideation.tsx b/auto-claude-ui/src/renderer/components/Ideation.tsx index 7f5639f7..1df25ce7 100644 --- a/auto-claude-ui/src/renderer/components/Ideation.tsx +++ b/auto-claude-ui/src/renderer/components/Ideation.tsx @@ -29,7 +29,8 @@ import { Database, Wifi, Box, - HardDrive + HardDrive, + Code2 } from 'lucide-react'; import { Button } from './ui/button'; import { Badge } from './ui/badge'; @@ -51,6 +52,7 @@ import { DialogHeader, DialogTitle } from './ui/dialog'; +import { EnvConfigModal, useClaudeTokenCheck } from './EnvConfigModal'; import { useIdeationStore, loadIdeation, @@ -75,7 +77,9 @@ import { UIUX_CATEGORY_LABELS, DOCUMENTATION_CATEGORY_LABELS, SECURITY_CATEGORY_LABELS, - PERFORMANCE_CATEGORY_LABELS + PERFORMANCE_CATEGORY_LABELS, + CODE_QUALITY_CATEGORY_LABELS, + CODE_QUALITY_SEVERITY_COLORS } from '../../shared/constants'; import type { Idea, @@ -86,7 +90,8 @@ import type { HighValueFeatureIdea, DocumentationGapIdea, SecurityHardeningIdea, - PerformanceOptimizationIdea + PerformanceOptimizationIdea, + CodeQualityIdea } from '../../shared/types'; interface IdeationProps { @@ -107,6 +112,8 @@ const TypeIcon = ({ type }: { type: IdeationType }) => { return ; case 'performance_optimizations': return ; + case 'code_quality': + return ; default: return ; } @@ -119,7 +126,8 @@ const ALL_IDEATION_TYPES: IdeationType[] = [ 'high_value_features', 'documentation_gaps', 'security_hardening', - 'performance_optimizations' + 'performance_optimizations', + 'code_quality' ]; // Type guard functions for new types @@ -131,6 +139,10 @@ function isSecurityHardeningIdea(idea: Idea): idea is SecurityHardeningIdea { return idea.type === 'security_hardening'; } +function isCodeQualityIdea(idea: Idea): idea is CodeQualityIdea { + return idea.type === 'code_quality'; +} + function isPerformanceOptimizationIdea(idea: Idea): idea is PerformanceOptimizationIdea { return idea.type === 'performance_optimizations'; } @@ -217,6 +229,11 @@ export function Ideation({ projectId }: IdeationProps) { const [activeTab, setActiveTab] = useState('all'); const [showConfigDialog, setShowConfigDialog] = useState(false); const [showDismissed, setShowDismissed] = useState(false); + const [showEnvConfigModal, setShowEnvConfigModal] = useState(false); + const [pendingAction, setPendingAction] = useState<'generate' | 'refresh' | null>(null); + + // Check if Claude token is configured + const { hasToken, isLoading: isCheckingToken, checkToken } = useClaudeTokenCheck(); // Set up IPC listeners and load ideation on mount useEffect(() => { @@ -225,14 +242,37 @@ export function Ideation({ projectId }: IdeationProps) { return cleanup; }, [projectId]); - const handleGenerate = () => { + const handleGenerate = async () => { + // Check token before generating + if (hasToken === false) { + setPendingAction('generate'); + setShowEnvConfigModal(true); + return; + } generateIdeation(projectId); }; - const handleRefresh = () => { + const handleRefresh = async () => { + // Check token before refreshing + if (hasToken === false) { + setPendingAction('refresh'); + setShowEnvConfigModal(true); + return; + } refreshIdeation(projectId); }; + // Handle when env config is complete - execute pending action + const handleEnvConfigured = () => { + checkToken(); // Re-check the token + if (pendingAction === 'generate') { + generateIdeation(projectId); + } else if (pendingAction === 'refresh') { + refreshIdeation(projectId); + } + setPendingAction(null); + }; + const handleConvertToTask = async (idea: Idea) => { const result = await window.electronAPI.convertIdeaToTask(projectId, idea.id); if (result.success) { @@ -315,11 +355,28 @@ export function Ideation({ projectId }: IdeationProps) { - + + {/* Show warning if token is missing */} + {hasToken === false && !isCheckingToken && ( +

+ + Claude token not configured. You'll be prompted to enter it when generating. +

+ )} + + {/* Environment Configuration Modal */} + ); } @@ -540,6 +597,15 @@ export function Ideation({ projectId }: IdeationProps) { + + {/* Environment Configuration Modal */} + ); } @@ -605,6 +671,11 @@ function IdeaCard({ idea, onClick, onConvert, onDismiss }: IdeaCardProps) { {(idea as PerformanceOptimizationIdea).impact} impact )} + {isCodeQualityIdea(idea) && ( + + {(idea as CodeQualityIdea).severity} + + )}

{idea.title} @@ -730,6 +801,10 @@ function IdeaDetailPanel({ idea, onClose, onConvert, onDismiss }: IdeaDetailPane {isPerformanceOptimizationIdea(idea) && ( )} + + {isCodeQualityIdea(idea) && ( + + )} {/* Actions */} @@ -1257,3 +1332,159 @@ function PerformanceOptimizationDetails({ idea }: { idea: PerformanceOptimizatio ); } + +function CodeQualityDetails({ idea }: { idea: CodeQualityIdea }) { + return ( + <> + {/* Metrics */} +
+ +
+ {idea.severity} +
+
Severity
+
+ +
+ {idea.estimatedEffort} +
+
Effort
+
+
+ + {/* Category */} +
+

+ + Category +

+ + {CODE_QUALITY_CATEGORY_LABELS[idea.category]} + +
+ + {/* Breaking Change Warning */} + {idea.breakingChange && ( +
+
+ + Breaking Change +
+

+ This refactoring may break existing code or tests. +

+
+ )} + + {/* Current State */} +
+

+ + Current State +

+

{idea.currentState}

+
+ + {/* Proposed Change */} +
+

+ + Proposed Change +

+

{idea.proposedChange}

+
+ + {/* Code Example */} + {idea.codeExample && ( +
+

+ + Code Example +

+
+            {idea.codeExample}
+          
+
+ )} + + {/* Metrics (if available) */} + {idea.metrics && ( +
+

Metrics

+
+ {idea.metrics.lineCount && ( + +
{idea.metrics.lineCount}
+
Lines
+
+ )} + {idea.metrics.complexity && ( + +
{idea.metrics.complexity}
+
Complexity
+
+ )} + {idea.metrics.duplicateLines && ( + +
{idea.metrics.duplicateLines}
+
Duplicate Lines
+
+ )} + {idea.metrics.testCoverage !== undefined && ( + +
{idea.metrics.testCoverage}%
+
Test Coverage
+
+ )} +
+
+ )} + + {/* Affected Files */} + {idea.affectedFiles.length > 0 && ( +
+

+ + Affected Files +

+
    + {idea.affectedFiles.map((file, i) => ( +
  • + {file} +
  • + ))} +
+
+ )} + + {/* Best Practice */} + {idea.bestPractice && ( +
+

+ + Best Practice +

+

{idea.bestPractice}

+
+ )} + + {/* Prerequisites */} + {idea.prerequisites && idea.prerequisites.length > 0 && ( +
+

+ + Prerequisites +

+
    + {idea.prerequisites.map((prereq, i) => ( +
  • + • + {prereq} +
  • + ))} +
+
+ )} + + ); +} diff --git a/auto-claude-ui/src/renderer/components/ProjectSettings.tsx b/auto-claude-ui/src/renderer/components/ProjectSettings.tsx index d97dc882..b2c20e5e 100644 --- a/auto-claude-ui/src/renderer/components/ProjectSettings.tsx +++ b/auto-claude-ui/src/renderer/components/ProjectSettings.tsx @@ -17,7 +17,8 @@ import { ChevronUp, Import, Radio, - Github + Github, + Globe } from 'lucide-react'; import { LinearTaskImportModal } from './LinearTaskImportModal'; import { @@ -517,18 +518,35 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings {/* Manual OAuth Token */}
- -

- Or paste a token directly if you have one from claude setup-token -

+
+ + {envConfig.claudeTokenIsGlobal && ( + + + Using global token + + )} +
+ {envConfig.claudeTokenIsGlobal ? ( +

+ Using token from App Settings. Enter a project-specific token below to override. +

+ ) : ( +

+ Paste a token from claude setup-token +

+ )}
updateEnvConfig({ claudeOAuthToken: e.target.value })} + placeholder={envConfig.claudeTokenIsGlobal ? 'Enter to override global token...' : 'your-oauth-token-here'} + value={envConfig.claudeTokenIsGlobal ? '' : (envConfig.claudeOAuthToken || '')} + onChange={(e) => updateEnvConfig({ + claudeOAuthToken: e.target.value || undefined, + // When user enters a value, it's no longer global + })} className="pr-10" />