mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
Merge branch 'main' into feat/mass-edit-backlog-features
This commit is contained in:
@@ -42,6 +42,9 @@ export function useSpecRegeneration({
|
||||
}
|
||||
|
||||
if (event.type === 'spec_regeneration_complete') {
|
||||
// Only show toast if we're in active creation flow (not regular regeneration)
|
||||
const isCreationFlow = creatingSpecProjectPath !== null;
|
||||
|
||||
setSpecCreatingForProject(null);
|
||||
setShowSetupDialog(false);
|
||||
setProjectOverview('');
|
||||
@@ -49,9 +52,12 @@ export function useSpecRegeneration({
|
||||
// Clear onboarding state if we came from onboarding
|
||||
setNewProjectName('');
|
||||
setNewProjectPath('');
|
||||
toast.success('App specification created', {
|
||||
description: 'Your project is now set up and ready to go!',
|
||||
});
|
||||
|
||||
if (isCreationFlow) {
|
||||
toast.success('App specification created', {
|
||||
description: 'Your project is now set up and ready to go!',
|
||||
});
|
||||
}
|
||||
} else if (event.type === 'spec_regeneration_error') {
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error('Failed to create specification', {
|
||||
|
||||
@@ -22,6 +22,10 @@ interface BoardHeaderProps {
|
||||
isMounted: boolean;
|
||||
}
|
||||
|
||||
// Shared styles for header control containers
|
||||
const controlContainerClass =
|
||||
'flex items-center gap-1.5 px-3 h-8 rounded-md bg-secondary border border-border';
|
||||
|
||||
export function BoardHeader({
|
||||
projectName,
|
||||
maxConcurrency,
|
||||
@@ -60,10 +64,7 @@ export function BoardHeader({
|
||||
|
||||
{/* Concurrency Slider - only show after mount to prevent hydration issues */}
|
||||
{isMounted && (
|
||||
<div
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border"
|
||||
data-testid="concurrency-slider-container"
|
||||
>
|
||||
<div className={controlContainerClass} data-testid="concurrency-slider-container">
|
||||
<Bot className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Agents</span>
|
||||
<Slider
|
||||
@@ -86,7 +87,7 @@ export function BoardHeader({
|
||||
|
||||
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||
{isMounted && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary border border-border">
|
||||
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
||||
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
|
||||
Auto Mode
|
||||
</Label>
|
||||
|
||||
@@ -168,15 +168,39 @@ function TagFilter({
|
||||
|
||||
export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { generationJobs, removeSuggestionFromJob } = useIdeationStore();
|
||||
const generationJobs = useIdeationStore((s) => s.generationJobs);
|
||||
const removeSuggestionFromJob = useIdeationStore((s) => s.removeSuggestionFromJob);
|
||||
const [addingId, setAddingId] = useState<string | null>(null);
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
|
||||
// Separate generating/error jobs from ready jobs with suggestions
|
||||
const activeJobs = generationJobs.filter(
|
||||
(j) => j.status === 'generating' || j.status === 'error'
|
||||
// Get jobs for current project only (memoized to prevent unnecessary re-renders)
|
||||
const projectJobs = useMemo(
|
||||
() =>
|
||||
currentProject?.path
|
||||
? generationJobs.filter((job) => job.projectPath === currentProject.path)
|
||||
: [],
|
||||
[generationJobs, currentProject?.path]
|
||||
);
|
||||
const readyJobs = generationJobs.filter((j) => j.status === 'ready' && j.suggestions.length > 0);
|
||||
|
||||
// Separate jobs by status and compute counts in a single pass
|
||||
const { activeJobs, readyJobs, generatingCount } = useMemo(() => {
|
||||
const active: GenerationJob[] = [];
|
||||
const ready: GenerationJob[] = [];
|
||||
let generating = 0;
|
||||
|
||||
for (const job of projectJobs) {
|
||||
if (job.status === 'generating') {
|
||||
active.push(job);
|
||||
generating++;
|
||||
} else if (job.status === 'error') {
|
||||
active.push(job);
|
||||
} else if (job.status === 'ready' && job.suggestions.length > 0) {
|
||||
ready.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
return { activeJobs: active, readyJobs: ready, generatingCount: generating };
|
||||
}, [projectJobs]);
|
||||
|
||||
// Flatten all suggestions with their parent job
|
||||
const allSuggestions = useMemo(
|
||||
@@ -203,8 +227,6 @@ export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
|
||||
return allSuggestions.filter(({ job }) => selectedTags.has(job.prompt.title));
|
||||
}, [allSuggestions, selectedTags]);
|
||||
|
||||
const generatingCount = generationJobs.filter((j) => j.status === 'generating').length;
|
||||
|
||||
const handleToggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -316,6 +338,16 @@ export function IdeationDashboard({ onGenerateIdeas }: IdeationDashboardProps) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Generate More Ideas Button - shown when there are items */}
|
||||
{!isEmpty && (
|
||||
<div className="pt-2">
|
||||
<Button onClick={onGenerateIdeas} variant="outline" className="w-full gap-2">
|
||||
<Lightbulb className="w-4 h-4" />
|
||||
Generate More Ideas
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{isEmpty && (
|
||||
<Card>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* PromptList - List of prompts for a specific category
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { ArrowLeft, Lightbulb, Loader2, CheckCircle2 } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
|
||||
@@ -20,7 +20,10 @@ interface PromptListProps {
|
||||
|
||||
export function PromptList({ category, onBack }: PromptListProps) {
|
||||
const currentProject = useAppStore((s) => s.currentProject);
|
||||
const { setMode, addGenerationJob, updateJobStatus, generationJobs } = useIdeationStore();
|
||||
const generationJobs = useIdeationStore((s) => s.generationJobs);
|
||||
const setMode = useIdeationStore((s) => s.setMode);
|
||||
const addGenerationJob = useIdeationStore((s) => s.addGenerationJob);
|
||||
const updateJobStatus = useIdeationStore((s) => s.updateJobStatus);
|
||||
const [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
|
||||
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
||||
const navigate = useNavigate();
|
||||
@@ -32,9 +35,19 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
||||
|
||||
const prompts = getPromptsByCategory(category);
|
||||
|
||||
// Get jobs for current project only (memoized to prevent unnecessary re-renders)
|
||||
const projectJobs = useMemo(
|
||||
() =>
|
||||
currentProject?.path
|
||||
? generationJobs.filter((job) => job.projectPath === currentProject.path)
|
||||
: [],
|
||||
[generationJobs, currentProject?.path]
|
||||
);
|
||||
|
||||
// Check which prompts are already generating
|
||||
const generatingPromptIds = new Set(
|
||||
generationJobs.filter((j) => j.status === 'generating').map((j) => j.prompt.id)
|
||||
const generatingPromptIds = useMemo(
|
||||
() => new Set(projectJobs.filter((j) => j.status === 'generating').map((j) => j.prompt.id)),
|
||||
[projectJobs]
|
||||
);
|
||||
|
||||
const handleSelectPrompt = async (prompt: IdeationPrompt) => {
|
||||
@@ -48,7 +61,7 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
||||
setLoadingPromptId(prompt.id);
|
||||
|
||||
// Add a job and navigate to dashboard
|
||||
const jobId = addGenerationJob(prompt);
|
||||
const jobId = addGenerationJob(currentProject.path, prompt);
|
||||
setStartedPrompts((prev) => new Set(prev).add(prompt.id));
|
||||
|
||||
// Show toast and navigate to dashboard
|
||||
|
||||
@@ -32,6 +32,53 @@ export function useCliStatus() {
|
||||
|
||||
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
|
||||
|
||||
// Refresh Claude auth status from the server
|
||||
const refreshAuthStatus = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.setup?.getClaudeStatus) return;
|
||||
|
||||
try {
|
||||
const result = await api.setup.getClaudeStatus();
|
||||
if (result.success && result.auth) {
|
||||
// Cast to extended type that includes server-added fields
|
||||
const auth = result.auth as typeof result.auth & {
|
||||
oauthTokenValid?: boolean;
|
||||
apiKeyValid?: boolean;
|
||||
};
|
||||
// Map server method names to client method types
|
||||
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
|
||||
const validMethods = [
|
||||
'oauth_token_env',
|
||||
'oauth_token',
|
||||
'api_key',
|
||||
'api_key_env',
|
||||
'credentials_file',
|
||||
'cli_authenticated',
|
||||
'none',
|
||||
] as const;
|
||||
type AuthMethod = (typeof validMethods)[number];
|
||||
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
|
||||
? (auth.method as AuthMethod)
|
||||
: auth.authenticated
|
||||
? 'api_key'
|
||||
: 'none'; // Default authenticated to api_key, not none
|
||||
const authStatus = {
|
||||
authenticated: auth.authenticated,
|
||||
method,
|
||||
hasCredentialsFile: auth.hasCredentialsFile ?? false,
|
||||
oauthTokenValid:
|
||||
auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
|
||||
apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
|
||||
hasEnvOAuthToken: auth.hasEnvOAuthToken,
|
||||
hasEnvApiKey: auth.hasEnvApiKey,
|
||||
};
|
||||
setClaudeAuthStatus(authStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to refresh Claude auth status:', error);
|
||||
}
|
||||
}, [setClaudeAuthStatus]);
|
||||
|
||||
// Check CLI status on mount
|
||||
useEffect(() => {
|
||||
const checkCliStatus = async () => {
|
||||
@@ -48,54 +95,13 @@ export function useCliStatus() {
|
||||
}
|
||||
|
||||
// Check Claude auth status (re-fetch on mount to ensure persistence)
|
||||
if (api?.setup?.getClaudeStatus) {
|
||||
try {
|
||||
const result = await api.setup.getClaudeStatus();
|
||||
if (result.success && result.auth) {
|
||||
// Cast to extended type that includes server-added fields
|
||||
const auth = result.auth as typeof result.auth & {
|
||||
oauthTokenValid?: boolean;
|
||||
apiKeyValid?: boolean;
|
||||
};
|
||||
// Map server method names to client method types
|
||||
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
|
||||
const validMethods = [
|
||||
'oauth_token_env',
|
||||
'oauth_token',
|
||||
'api_key',
|
||||
'api_key_env',
|
||||
'credentials_file',
|
||||
'cli_authenticated',
|
||||
'none',
|
||||
] as const;
|
||||
type AuthMethod = (typeof validMethods)[number];
|
||||
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
|
||||
? (auth.method as AuthMethod)
|
||||
: auth.authenticated
|
||||
? 'api_key'
|
||||
: 'none'; // Default authenticated to api_key, not none
|
||||
const authStatus = {
|
||||
authenticated: auth.authenticated,
|
||||
method,
|
||||
hasCredentialsFile: auth.hasCredentialsFile ?? false,
|
||||
oauthTokenValid:
|
||||
auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
|
||||
apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
|
||||
hasEnvOAuthToken: auth.hasEnvOAuthToken,
|
||||
hasEnvApiKey: auth.hasEnvApiKey,
|
||||
};
|
||||
setClaudeAuthStatus(authStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to check Claude auth status:', error);
|
||||
}
|
||||
}
|
||||
await refreshAuthStatus();
|
||||
};
|
||||
|
||||
checkCliStatus();
|
||||
}, [setClaudeAuthStatus]);
|
||||
}, [refreshAuthStatus]);
|
||||
|
||||
// Refresh Claude CLI status
|
||||
// Refresh Claude CLI status and auth status
|
||||
const handleRefreshClaudeCli = useCallback(async () => {
|
||||
setIsCheckingClaudeCli(true);
|
||||
try {
|
||||
@@ -104,12 +110,14 @@ export function useCliStatus() {
|
||||
const status = await api.checkClaudeCli();
|
||||
setClaudeCliStatus(status);
|
||||
}
|
||||
// Also refresh auth status
|
||||
await refreshAuthStatus();
|
||||
} catch (error) {
|
||||
logger.error('Failed to refresh Claude CLI status:', error);
|
||||
} finally {
|
||||
setIsCheckingClaudeCli(false);
|
||||
}
|
||||
}, []);
|
||||
}, [refreshAuthStatus]);
|
||||
|
||||
return {
|
||||
claudeCliStatus,
|
||||
|
||||
@@ -8,6 +8,9 @@ interface UseCliStatusOptions {
|
||||
setAuthStatus: (status: any) => void;
|
||||
}
|
||||
|
||||
// Create logger once outside the hook to prevent infinite re-renders
|
||||
const logger = createLogger('CliStatus');
|
||||
|
||||
export function useCliStatus({
|
||||
cliType,
|
||||
statusApi,
|
||||
@@ -15,7 +18,6 @@ export function useCliStatus({
|
||||
setAuthStatus,
|
||||
}: UseCliStatusOptions) {
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const logger = createLogger('CliStatus');
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
logger.info(`Starting status check for ${cliType}...`);
|
||||
@@ -66,7 +68,7 @@ export function useCliStatus({
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
}, [cliType, statusApi, setCliStatus, setAuthStatus, logger]);
|
||||
}, [cliType, statusApi, setCliStatus, setAuthStatus]);
|
||||
|
||||
return { isChecking, checkStatus };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface ThemeStepProps {
|
||||
}
|
||||
|
||||
export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
||||
const { theme, setTheme, setPreviewTheme } = useAppStore();
|
||||
const { theme, setTheme, setPreviewTheme, currentProject, setProjectTheme } = useAppStore();
|
||||
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
|
||||
|
||||
const handleThemeHover = (themeValue: string) => {
|
||||
@@ -24,6 +24,11 @@ export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
||||
|
||||
const handleThemeClick = (themeValue: string) => {
|
||||
setTheme(themeValue as typeof theme);
|
||||
// Also update the current project's theme if one exists
|
||||
// This ensures the selected theme is visible since getEffectiveTheme() prioritizes project theme
|
||||
if (currentProject) {
|
||||
setProjectTheme(currentProject.id, themeValue as typeof theme);
|
||||
}
|
||||
setPreviewTheme(null);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export function SpecView() {
|
||||
const { currentProject, appSpec } = useAppStore();
|
||||
|
||||
// Loading state
|
||||
const { isLoading, specExists, loadSpec } = useSpecLoading();
|
||||
const { isLoading, specExists, isGenerationRunning, loadSpec } = useSpecLoading();
|
||||
|
||||
// Save state
|
||||
const { isSaving, hasChanges, saveSpec, handleChange, setHasChanges } = useSpecSave();
|
||||
@@ -82,15 +82,20 @@ export function SpecView() {
|
||||
);
|
||||
}
|
||||
|
||||
// Empty state - no spec exists
|
||||
if (!specExists) {
|
||||
// Empty state - no spec exists or generation is running
|
||||
// When generation is running, we skip loading the spec to avoid 500 errors,
|
||||
// so we show the empty state with generation indicator
|
||||
if (!specExists || isGenerationRunning) {
|
||||
// If generation is running (from loading hook check), ensure we show the generating UI
|
||||
const showAsGenerating = isCreating || isGenerationRunning;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpecEmptyState
|
||||
projectPath={currentProject.path}
|
||||
isCreating={isCreating}
|
||||
isRegenerating={isRegenerating}
|
||||
currentPhase={currentPhase}
|
||||
isCreating={showAsGenerating}
|
||||
isRegenerating={isRegenerating || isGenerationRunning}
|
||||
currentPhase={currentPhase || (isGenerationRunning ? 'initialization' : '')}
|
||||
errorMessage={errorMessage}
|
||||
onCreateClick={() => setShowCreateDialog(true)}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ export function useSpecLoading() {
|
||||
const { currentProject, setAppSpec } = useAppStore();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [specExists, setSpecExists] = useState(true);
|
||||
const [isGenerationRunning, setIsGenerationRunning] = useState(false);
|
||||
|
||||
const loadSpec = useCallback(async () => {
|
||||
if (!currentProject) return;
|
||||
@@ -16,6 +17,21 @@ export function useSpecLoading() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
|
||||
// Check if spec generation is running before trying to load
|
||||
// This prevents showing "No App Specification Found" during generation
|
||||
if (api.specRegeneration) {
|
||||
const status = await api.specRegeneration.status();
|
||||
if (status.success && status.isRunning) {
|
||||
logger.debug('Spec generation is running, skipping load');
|
||||
setIsGenerationRunning(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Always reset when generation is not running (handles edge case where api.specRegeneration might not be available)
|
||||
setIsGenerationRunning(false);
|
||||
|
||||
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
|
||||
|
||||
if (result.success && result.content) {
|
||||
@@ -42,6 +58,7 @@ export function useSpecLoading() {
|
||||
isLoading,
|
||||
specExists,
|
||||
setSpecExists,
|
||||
isGenerationRunning,
|
||||
loadSpec,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user