mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,12 +7,28 @@ import { createLogger } from '@automaker/utils/logger';
|
||||
import { getHttpApiClient } from './http-api-client';
|
||||
import { getElectronAPI } from './electron';
|
||||
import { getItem, setItem } from './storage';
|
||||
import path from 'path';
|
||||
|
||||
const logger = createLogger('WorkspaceConfig');
|
||||
|
||||
const LAST_PROJECT_DIR_KEY = 'automaker:lastProjectDir';
|
||||
|
||||
/**
|
||||
* Browser-compatible path join utility
|
||||
* Works in both Node.js and browser environments
|
||||
*/
|
||||
function joinPath(...parts: string[]): string {
|
||||
// Remove empty parts and normalize separators
|
||||
const normalized = parts
|
||||
.filter((p) => p)
|
||||
.map((p) => p.replace(/\\/g, '/'))
|
||||
.join('/')
|
||||
.replace(/\/+/g, '/'); // Remove duplicate slashes
|
||||
|
||||
// Preserve leading slash if first part had it
|
||||
const hasLeadingSlash = parts[0]?.startsWith('/');
|
||||
return hasLeadingSlash ? '/' + normalized.replace(/^\//, '') : normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default Documents/Automaker directory path
|
||||
* @returns Promise resolving to Documents/Automaker path, or null if unavailable
|
||||
@@ -21,7 +37,7 @@ async function getDefaultDocumentsPath(): Promise<string | null> {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const documentsPath = await api.getPath('documents');
|
||||
return path.join(documentsPath, 'Automaker');
|
||||
return joinPath(documentsPath, 'Automaker');
|
||||
} catch (error) {
|
||||
logger.error('Failed to get documents path:', error);
|
||||
return null;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Toaster } from 'sonner';
|
||||
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||
import { SandboxRiskDialog } from '@/components/dialogs/sandbox-risk-dialog';
|
||||
import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-screen';
|
||||
import { LoadingState } from '@/components/ui/loading-state';
|
||||
|
||||
const logger = createLogger('RootLayout');
|
||||
|
||||
@@ -330,7 +331,7 @@ function RootLayoutContent() {
|
||||
if (sandboxStatus === 'pending') {
|
||||
return (
|
||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||
<div className="text-muted-foreground">Checking environment...</div>
|
||||
<LoadingState message="Checking environment..." />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -354,7 +355,7 @@ function RootLayoutContent() {
|
||||
if (!isElectronMode() && !authChecked) {
|
||||
return (
|
||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||
<div className="text-muted-foreground">Loading...</div>
|
||||
<LoadingState message="Loading..." />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -364,7 +365,7 @@ function RootLayoutContent() {
|
||||
if (!isElectronMode() && !isAuthenticated) {
|
||||
return (
|
||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||
<div className="text-muted-foreground">Redirecting to login...</div>
|
||||
<LoadingState message="Redirecting to login..." />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type GenerationJobStatus = 'generating' | 'ready' | 'error';
|
||||
|
||||
export interface GenerationJob {
|
||||
id: string;
|
||||
projectPath: string;
|
||||
prompt: IdeationPrompt;
|
||||
status: GenerationJobStatus;
|
||||
suggestions: AnalysisSuggestion[];
|
||||
@@ -76,7 +77,7 @@ interface IdeationActions {
|
||||
getSelectedIdea: () => Idea | null;
|
||||
|
||||
// Generation Jobs
|
||||
addGenerationJob: (prompt: IdeationPrompt) => string;
|
||||
addGenerationJob: (projectPath: string, prompt: IdeationPrompt) => string;
|
||||
updateJobStatus: (
|
||||
jobId: string,
|
||||
status: GenerationJobStatus,
|
||||
@@ -172,10 +173,11 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
},
|
||||
|
||||
// Generation Jobs
|
||||
addGenerationJob: (prompt) => {
|
||||
const jobId = `job-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
addGenerationJob: (projectPath, prompt) => {
|
||||
const jobId = `job-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
const job: GenerationJob = {
|
||||
id: jobId,
|
||||
projectPath,
|
||||
prompt,
|
||||
status: 'generating',
|
||||
suggestions: [],
|
||||
@@ -311,7 +313,7 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
}),
|
||||
{
|
||||
name: 'automaker-ideation-store',
|
||||
version: 3,
|
||||
version: 4,
|
||||
partialize: (state) => ({
|
||||
// Only persist these fields
|
||||
ideas: state.ideas,
|
||||
@@ -319,6 +321,18 @@ export const useIdeationStore = create<IdeationState & IdeationActions>()(
|
||||
analysisResult: state.analysisResult,
|
||||
filterStatus: state.filterStatus,
|
||||
}),
|
||||
migrate: (persistedState: unknown, version: number) => {
|
||||
const state = persistedState as Record<string, unknown>;
|
||||
if (version < 4) {
|
||||
// Remove legacy jobs that don't have projectPath (from before project-scoping was added)
|
||||
const jobs = (state.generationJobs as GenerationJob[]) || [];
|
||||
return {
|
||||
...state,
|
||||
generationJobs: jobs.filter((job) => job.projectPath !== undefined),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user