mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge branch 'main' into random-fixes
This commit is contained in:
@@ -33,6 +33,7 @@ interface BrowseResult {
|
||||
directories: DirectoryEntry[];
|
||||
drives?: string[];
|
||||
error?: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
interface FileBrowserDialogProps {
|
||||
@@ -41,6 +42,7 @@ interface FileBrowserDialogProps {
|
||||
onSelect: (path: string) => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
initialPath?: string;
|
||||
}
|
||||
|
||||
export function FileBrowserDialog({
|
||||
@@ -49,6 +51,7 @@ export function FileBrowserDialog({
|
||||
onSelect,
|
||||
title = "Select Project Directory",
|
||||
description = "Navigate to your project folder or paste a path directly",
|
||||
initialPath,
|
||||
}: FileBrowserDialogProps) {
|
||||
const [currentPath, setCurrentPath] = useState<string>("");
|
||||
const [pathInput, setPathInput] = useState<string>("");
|
||||
@@ -57,11 +60,13 @@ export function FileBrowserDialog({
|
||||
const [drives, setDrives] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [warning, setWarning] = useState("");
|
||||
const pathInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const browseDirectory = async (dirPath?: string) => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setWarning("");
|
||||
|
||||
try {
|
||||
// Get server URL from environment or default
|
||||
@@ -82,6 +87,7 @@ export function FileBrowserDialog({
|
||||
setParentPath(result.parentPath);
|
||||
setDirectories(result.directories);
|
||||
setDrives(result.drives || []);
|
||||
setWarning(result.warning || "");
|
||||
} else {
|
||||
setError(result.error || "Failed to browse directory");
|
||||
}
|
||||
@@ -94,13 +100,25 @@ export function FileBrowserDialog({
|
||||
}
|
||||
};
|
||||
|
||||
// Load home directory on mount
|
||||
// Reset current path when dialog closes
|
||||
useEffect(() => {
|
||||
if (open && !currentPath) {
|
||||
browseDirectory();
|
||||
if (!open) {
|
||||
setCurrentPath("");
|
||||
setPathInput("");
|
||||
setParentPath(null);
|
||||
setDirectories([]);
|
||||
setError("");
|
||||
setWarning("");
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// Load initial path or home directory when dialog opens
|
||||
useEffect(() => {
|
||||
if (open && !currentPath) {
|
||||
browseDirectory(initialPath);
|
||||
}
|
||||
}, [open, initialPath]);
|
||||
|
||||
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
||||
browseDirectory(dir.path);
|
||||
};
|
||||
@@ -246,7 +264,13 @@ export function FileBrowserDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && directories.length === 0 && (
|
||||
{warning && (
|
||||
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg mb-2">
|
||||
<div className="text-sm text-yellow-500">{warning}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && !warning && directories.length === 0 && (
|
||||
<div className="flex items-center justify-center h-full p-8">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No subdirectories found
|
||||
|
||||
@@ -34,6 +34,10 @@ import {
|
||||
Sparkles,
|
||||
Loader2,
|
||||
Terminal,
|
||||
Rocket,
|
||||
Zap,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -78,6 +82,7 @@ import { themeOptions } from "@/config/theme-options";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||
import { NewProjectModal } from "@/components/new-project-modal";
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
@@ -92,6 +97,8 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { getHttpApiClient } from "@/lib/http-api-client";
|
||||
import type { StarterTemplate } from "@/lib/templates";
|
||||
|
||||
interface NavSection {
|
||||
label?: string;
|
||||
@@ -205,6 +212,8 @@ export function Sidebar() {
|
||||
setPreviewTheme,
|
||||
theme: globalTheme,
|
||||
moveProjectToTrash,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
} = useAppStore();
|
||||
|
||||
// Environment variable flags for hiding sidebar items
|
||||
@@ -234,17 +243,26 @@ export function Sidebar() {
|
||||
// State for running agents count
|
||||
const [runningAgentsCount, setRunningAgentsCount] = useState(0);
|
||||
|
||||
// State for new project modal
|
||||
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
|
||||
const [isCreatingProject, setIsCreatingProject] = useState(false);
|
||||
|
||||
// State for new project onboarding dialog
|
||||
const [showOnboardingDialog, setShowOnboardingDialog] = useState(false);
|
||||
const [newProjectName, setNewProjectName] = useState("");
|
||||
const [newProjectPath, setNewProjectPath] = useState("");
|
||||
|
||||
// State for new project setup dialog
|
||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||
const [setupProjectPath, setSetupProjectPath] = useState("");
|
||||
const [projectOverview, setProjectOverview] = useState("");
|
||||
const [isCreatingSpec, setIsCreatingSpec] = useState(false);
|
||||
const [creatingSpecProjectPath, setCreatingSpecProjectPath] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
const [showSpecIndicator, setShowSpecIndicator] = useState(true);
|
||||
|
||||
// Derive isCreatingSpec from store state
|
||||
const isCreatingSpec = specCreatingForProject !== null;
|
||||
const creatingSpecProjectPath = specCreatingForProject;
|
||||
|
||||
// Ref for project search input
|
||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -334,22 +352,39 @@ export function Sidebar() {
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent(
|
||||
(event: SpecRegenerationEvent) => {
|
||||
console.log("[Sidebar] Spec regeneration event:", event.type);
|
||||
console.log(
|
||||
"[Sidebar] Spec regeneration event:",
|
||||
event.type,
|
||||
"for project:",
|
||||
event.projectPath
|
||||
);
|
||||
|
||||
// Only handle events for the project we're currently setting up
|
||||
if (
|
||||
event.projectPath !== creatingSpecProjectPath &&
|
||||
event.projectPath !== setupProjectPath
|
||||
) {
|
||||
console.log(
|
||||
"[Sidebar] Ignoring event - not for project being set up"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "spec_regeneration_complete") {
|
||||
setIsCreatingSpec(false);
|
||||
setCreatingSpecProjectPath(null);
|
||||
setSpecCreatingForProject(null);
|
||||
setShowSetupDialog(false);
|
||||
setProjectOverview("");
|
||||
setSetupProjectPath("");
|
||||
// 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!",
|
||||
});
|
||||
// Navigate to spec view to show the new spec
|
||||
setCurrentView("spec");
|
||||
} else if (event.type === "spec_regeneration_error") {
|
||||
setIsCreatingSpec(false);
|
||||
setCreatingSpecProjectPath(null);
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error("Failed to create specification", {
|
||||
description: event.error,
|
||||
});
|
||||
@@ -360,7 +395,12 @@ export function Sidebar() {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [setCurrentView]);
|
||||
}, [
|
||||
setCurrentView,
|
||||
creatingSpecProjectPath,
|
||||
setupProjectPath,
|
||||
setSpecCreatingForProject,
|
||||
]);
|
||||
|
||||
// Fetch running agents count function - used for initial load and event-driven updates
|
||||
const fetchRunningAgentsCount = useCallback(async () => {
|
||||
@@ -409,8 +449,8 @@ export function Sidebar() {
|
||||
const handleCreateInitialSpec = useCallback(async () => {
|
||||
if (!setupProjectPath || !projectOverview.trim()) return;
|
||||
|
||||
setIsCreatingSpec(true);
|
||||
setCreatingSpecProjectPath(setupProjectPath);
|
||||
// Set store state immediately so the loader shows up right away
|
||||
setSpecCreatingForProject(setupProjectPath);
|
||||
setShowSpecIndicator(true);
|
||||
setShowSetupDialog(false);
|
||||
|
||||
@@ -418,8 +458,7 @@ export function Sidebar() {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) {
|
||||
toast.error("Spec regeneration not available");
|
||||
setIsCreatingSpec(false);
|
||||
setCreatingSpecProjectPath(null);
|
||||
setSpecCreatingForProject(null);
|
||||
return;
|
||||
}
|
||||
const result = await api.specRegeneration.create(
|
||||
@@ -430,8 +469,7 @@ export function Sidebar() {
|
||||
|
||||
if (!result.success) {
|
||||
console.error("[Sidebar] Failed to start spec creation:", result.error);
|
||||
setIsCreatingSpec(false);
|
||||
setCreatingSpecProjectPath(null);
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error("Failed to create specification", {
|
||||
description: result.error,
|
||||
});
|
||||
@@ -439,24 +477,345 @@ export function Sidebar() {
|
||||
// If successful, we'll wait for the events to update the state
|
||||
} catch (error) {
|
||||
console.error("[Sidebar] Failed to create spec:", error);
|
||||
setIsCreatingSpec(false);
|
||||
setCreatingSpecProjectPath(null);
|
||||
setSpecCreatingForProject(null);
|
||||
toast.error("Failed to create specification", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}, [setupProjectPath, projectOverview]);
|
||||
}, [setupProjectPath, projectOverview, setSpecCreatingForProject]);
|
||||
|
||||
// Handle skipping setup
|
||||
const handleSkipSetup = useCallback(() => {
|
||||
setShowSetupDialog(false);
|
||||
setProjectOverview("");
|
||||
setSetupProjectPath("");
|
||||
// Clear onboarding state if we came from onboarding
|
||||
if (newProjectPath) {
|
||||
setNewProjectName("");
|
||||
setNewProjectPath("");
|
||||
}
|
||||
toast.info("Setup skipped", {
|
||||
description: "You can set up your app_spec.txt later from the Spec view.",
|
||||
});
|
||||
}, [newProjectPath]);
|
||||
|
||||
// Handle onboarding dialog - generate spec
|
||||
const handleOnboardingGenerateSpec = useCallback(() => {
|
||||
setShowOnboardingDialog(false);
|
||||
// Navigate to the setup dialog flow
|
||||
setSetupProjectPath(newProjectPath);
|
||||
setProjectOverview("");
|
||||
setShowSetupDialog(true);
|
||||
}, [newProjectPath]);
|
||||
|
||||
// Handle onboarding dialog - skip
|
||||
const handleOnboardingSkip = useCallback(() => {
|
||||
setShowOnboardingDialog(false);
|
||||
setNewProjectName("");
|
||||
setNewProjectPath("");
|
||||
toast.info(
|
||||
"You can generate your app_spec.txt anytime from the Spec view",
|
||||
{
|
||||
description: "Your project is ready to use!",
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Create a blank project with just .automaker directory structure
|
||||
*/
|
||||
const handleCreateBlankProject = useCallback(
|
||||
async (projectName: string, parentDir: string) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
const projectPath = `${parentDir}/${projectName}`;
|
||||
|
||||
// Create project directory
|
||||
const mkdirResult = await api.mkdir(projectPath);
|
||||
if (!mkdirResult.success) {
|
||||
toast.error("Failed to create project directory", {
|
||||
description: mkdirResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize .automaker directory with all necessary files
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with the project name
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
<project_name>${projectName}</project_name>
|
||||
|
||||
<overview>
|
||||
Describe your project here. This file will be analyzed by an AI agent
|
||||
to understand your project structure and tech stack.
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<!-- The AI agent will fill this in after analyzing your project -->
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<!-- List core features and capabilities -->
|
||||
</core_capabilities>
|
||||
|
||||
<implemented_features>
|
||||
<!-- The AI agent will populate this based on code analysis -->
|
||||
</implemented_features>
|
||||
</project_specification>`
|
||||
);
|
||||
|
||||
const trashedProject = trashedProjects.find(
|
||||
(p) => p.path === projectPath
|
||||
);
|
||||
const effectiveTheme =
|
||||
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||
(currentProject?.theme as ThemeMode | undefined) ||
|
||||
globalTheme;
|
||||
const project = upsertAndSetCurrentProject(
|
||||
projectPath,
|
||||
projectName,
|
||||
effectiveTheme
|
||||
);
|
||||
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
// Show onboarding dialog for new project
|
||||
setNewProjectName(projectName);
|
||||
setNewProjectPath(projectPath);
|
||||
setShowOnboardingDialog(true);
|
||||
|
||||
toast.success("Project created", {
|
||||
description: `Created ${projectName} with .automaker directory`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Sidebar] Failed to create project:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a project from a GitHub starter template
|
||||
*/
|
||||
const handleCreateFromTemplate = useCallback(
|
||||
async (
|
||||
template: StarterTemplate,
|
||||
projectName: string,
|
||||
parentDir: string
|
||||
) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
const api = getElectronAPI();
|
||||
|
||||
// Clone the template repository
|
||||
const cloneResult = await httpClient.templates.clone(
|
||||
template.repoUrl,
|
||||
projectName,
|
||||
parentDir
|
||||
);
|
||||
|
||||
if (!cloneResult.success || !cloneResult.projectPath) {
|
||||
toast.error("Failed to clone template", {
|
||||
description: cloneResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const projectPath = cloneResult.projectPath;
|
||||
|
||||
// Initialize .automaker directory with all necessary files
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with template-specific info
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
<project_name>${projectName}</project_name>
|
||||
|
||||
<overview>
|
||||
This project was created from the "${template.name}" starter template.
|
||||
${template.description}
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
${template.techStack
|
||||
.map((tech) => `<technology>${tech}</technology>`)
|
||||
.join("\n ")}
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
${template.features
|
||||
.map((feature) => `<capability>${feature}</capability>`)
|
||||
.join("\n ")}
|
||||
</core_capabilities>
|
||||
|
||||
<implemented_features>
|
||||
<!-- The AI agent will populate this based on code analysis -->
|
||||
</implemented_features>
|
||||
</project_specification>`
|
||||
);
|
||||
|
||||
const trashedProject = trashedProjects.find(
|
||||
(p) => p.path === projectPath
|
||||
);
|
||||
const effectiveTheme =
|
||||
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||
(currentProject?.theme as ThemeMode | undefined) ||
|
||||
globalTheme;
|
||||
const project = upsertAndSetCurrentProject(
|
||||
projectPath,
|
||||
projectName,
|
||||
effectiveTheme
|
||||
);
|
||||
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
// Show onboarding dialog for new project
|
||||
setNewProjectName(projectName);
|
||||
setNewProjectPath(projectPath);
|
||||
setShowOnboardingDialog(true);
|
||||
|
||||
toast.success("Project created from template", {
|
||||
description: `Created ${projectName} from ${template.name}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[Sidebar] Failed to create project from template:",
|
||||
error
|
||||
);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a project from a custom GitHub URL
|
||||
*/
|
||||
const handleCreateFromCustomUrl = useCallback(
|
||||
async (repoUrl: string, projectName: string, parentDir: string) => {
|
||||
setIsCreatingProject(true);
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
const api = getElectronAPI();
|
||||
|
||||
// Clone the repository
|
||||
const cloneResult = await httpClient.templates.clone(
|
||||
repoUrl,
|
||||
projectName,
|
||||
parentDir
|
||||
);
|
||||
|
||||
if (!cloneResult.success || !cloneResult.projectPath) {
|
||||
toast.error("Failed to clone repository", {
|
||||
description: cloneResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const projectPath = cloneResult.projectPath;
|
||||
|
||||
// Initialize .automaker directory with all necessary files
|
||||
const initResult = await initializeProject(projectPath);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error("Failed to initialize project", {
|
||||
description: initResult.error || "Unknown error occurred",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with basic info
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
<project_name>${projectName}</project_name>
|
||||
|
||||
<overview>
|
||||
This project was cloned from ${repoUrl}.
|
||||
The AI agent will analyze the project structure.
|
||||
</overview>
|
||||
|
||||
<technology_stack>
|
||||
<!-- The AI agent will fill this in after analyzing your project -->
|
||||
</technology_stack>
|
||||
|
||||
<core_capabilities>
|
||||
<!-- List core features and capabilities -->
|
||||
</core_capabilities>
|
||||
|
||||
<implemented_features>
|
||||
<!-- The AI agent will populate this based on code analysis -->
|
||||
</implemented_features>
|
||||
</project_specification>`
|
||||
);
|
||||
|
||||
const trashedProject = trashedProjects.find(
|
||||
(p) => p.path === projectPath
|
||||
);
|
||||
const effectiveTheme =
|
||||
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||
(currentProject?.theme as ThemeMode | undefined) ||
|
||||
globalTheme;
|
||||
const project = upsertAndSetCurrentProject(
|
||||
projectPath,
|
||||
projectName,
|
||||
effectiveTheme
|
||||
);
|
||||
|
||||
setShowNewProjectModal(false);
|
||||
|
||||
// Show onboarding dialog for new project
|
||||
setNewProjectName(projectName);
|
||||
setNewProjectPath(projectPath);
|
||||
setShowOnboardingDialog(true);
|
||||
|
||||
toast.success("Project created from repository", {
|
||||
description: `Created ${projectName} from ${repoUrl}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Sidebar] Failed to create project from URL:", error);
|
||||
toast.error("Failed to create project", {
|
||||
description: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingProject(false);
|
||||
}
|
||||
},
|
||||
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject]
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens the system folder selection dialog and initializes the selected project.
|
||||
* Used by both the 'O' keyboard shortcut and the folder icon button.
|
||||
@@ -871,7 +1230,7 @@ export function Sidebar() {
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="A"
|
||||
className="h-[1.3em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform"
|
||||
className="h-[1.8em] w-auto inline-block align-middle group-hover:rotate-12 transition-transform"
|
||||
/>
|
||||
<span className="-ml-0.5">
|
||||
uto<span className="text-brand-500">maker</span>
|
||||
@@ -899,7 +1258,7 @@ export function Sidebar() {
|
||||
{sidebarOpen && (
|
||||
<div className="flex items-center gap-2 titlebar-no-drag px-2 mt-3">
|
||||
<button
|
||||
onClick={() => setCurrentView("welcome")}
|
||||
onClick={() => setShowNewProjectModal(true)}
|
||||
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
|
||||
title="New Project"
|
||||
data-testid="new-project-button"
|
||||
@@ -1621,27 +1980,112 @@ export function Sidebar() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Spec Creation Indicator - Bottom Right Toast */}
|
||||
{isCreatingSpec &&
|
||||
showSpecIndicator &&
|
||||
currentProject?.path === creatingSpecProjectPath && (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex items-center gap-3 bg-card border border-border rounded-lg shadow-lg p-4 max-w-sm">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">Creating App Specification</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
Working on your project...
|
||||
{/* New Project Onboarding Dialog */}
|
||||
<Dialog
|
||||
open={showOnboardingDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleOnboardingSkip();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-brand-500/10 border border-brand-500/20">
|
||||
<Rocket className="w-6 h-6 text-brand-500" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-2xl">
|
||||
Welcome to {newProjectName}!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground mt-1">
|
||||
Your new project is ready. Let's get you started.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-6">
|
||||
{/* Main explanation */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-foreground leading-relaxed">
|
||||
Would you like to auto-generate your{" "}
|
||||
<strong>app_spec.txt</strong>? This file helps describe your
|
||||
project and is used to pre-populate your backlog with features
|
||||
to work on.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Benefits list */}
|
||||
<div className="space-y-3 rounded-lg bg-muted/50 border border-border p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Pre-populate your backlog
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Automatically generate features based on your project
|
||||
specification
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Zap className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Better AI assistance
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Help AI agents understand your project structure and tech
|
||||
stack
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
Project documentation
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Keep a clear record of your project's capabilities and
|
||||
features
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info box */}
|
||||
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-3">
|
||||
<p className="text-xs text-blue-400 leading-relaxed">
|
||||
<strong className="text-blue-300">Tip:</strong> You can always
|
||||
generate or edit your app_spec.txt later from the Spec Editor in
|
||||
the sidebar.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowSpecIndicator(false)}
|
||||
className="p-1 hover:bg-muted rounded-md transition-colors flex-shrink-0"
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
<X className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleOnboardingSkip}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOnboardingGenerateSpec}
|
||||
className="bg-gradient-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-white border-0"
|
||||
>
|
||||
<Sparkles className="w-4 h-4 mr-2" />
|
||||
Generate App Spec
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Project Confirmation Dialog */}
|
||||
<DeleteProjectDialog
|
||||
@@ -1650,6 +2094,16 @@ export function Sidebar() {
|
||||
project={currentProject}
|
||||
onConfirm={moveProjectToTrash}
|
||||
/>
|
||||
|
||||
{/* New Project Modal */}
|
||||
<NewProjectModal
|
||||
open={showNewProjectModal}
|
||||
onOpenChange={setShowNewProjectModal}
|
||||
onCreateBlankProject={handleCreateBlankProject}
|
||||
onCreateFromTemplate={handleCreateFromTemplate}
|
||||
onCreateFromCustomUrl={handleCreateFromCustomUrl}
|
||||
isCreating={isCreatingProject}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ export function NewProjectModal({
|
||||
title: "Select Base Project Directory",
|
||||
description:
|
||||
"Choose the parent directory where your project will be created",
|
||||
initialPath: workspaceDir || undefined,
|
||||
});
|
||||
if (selectedPath) {
|
||||
setWorkspaceDir(selectedPath);
|
||||
@@ -281,7 +282,7 @@ export function NewProjectModal({
|
||||
<>
|
||||
Will be created at:{" "}
|
||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded truncate">
|
||||
{projectPath || "..."}
|
||||
{projectPath || workspaceDir}
|
||||
</code>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -345,6 +345,7 @@ export function AnalysisView() {
|
||||
const techStack = detectTechStack();
|
||||
|
||||
// Generate the spec content
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
const specContent = `<project_specification>
|
||||
<project_name>${projectName}</project_name>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
@@ -179,6 +180,8 @@ export function BoardView() {
|
||||
kanbanCardDetailLevel,
|
||||
setKanbanCardDetailLevel,
|
||||
boardBackgroundByProject,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
} = useAppStore();
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
|
||||
@@ -233,6 +236,9 @@ export function BoardView() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
// Validation state for add feature form
|
||||
const [descriptionError, setDescriptionError] = useState(false);
|
||||
// Derive spec creation state from store - check if current project is the one being created
|
||||
const isCreatingSpec = specCreatingForProject === currentProject?.path;
|
||||
const creatingSpecProjectPath = specCreatingForProject;
|
||||
|
||||
// Make current project available globally for modal
|
||||
useEffect(() => {
|
||||
@@ -264,6 +270,37 @@ export function BoardView() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Subscribe to spec regeneration events to clear state on completion
|
||||
useEffect(() => {
|
||||
const api = getElectronAPI();
|
||||
if (!api.specRegeneration) return;
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||
console.log(
|
||||
"[BoardView] Spec regeneration event:",
|
||||
event.type,
|
||||
"for project:",
|
||||
event.projectPath
|
||||
);
|
||||
|
||||
// Only handle completion/error events for the project being created
|
||||
// The creating state is set by sidebar when user initiates the action
|
||||
if (event.projectPath !== specCreatingForProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "spec_regeneration_complete") {
|
||||
setSpecCreatingForProject(null);
|
||||
} else if (event.type === "spec_regeneration_error") {
|
||||
setSpecCreatingForProject(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [specCreatingForProject, setSpecCreatingForProject]);
|
||||
|
||||
// Track previous project to detect switches
|
||||
const prevProjectPathRef = useRef<string | null>(null);
|
||||
const isSwitchingProjectRef = useRef<boolean>(false);
|
||||
@@ -1791,34 +1828,50 @@ export function BoardView() {
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Search Bar Row */}
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||
<div className="relative max-w-md flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search features by keyword..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-12 border-border"
|
||||
data-testid="kanban-search-input"
|
||||
/>
|
||||
{searchQuery ? (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
||||
data-testid="kanban-search-clear"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
|
||||
data-testid="kanban-search-hotkey"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
<div className="relative max-w-md flex-1 flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search features by keyword..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-12 border-border"
|
||||
data-testid="kanban-search-input"
|
||||
/>
|
||||
{searchQuery ? (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-sm hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
||||
data-testid="kanban-search-clear"
|
||||
aria-label="Clear search"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
|
||||
data-testid="kanban-search-hotkey"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Spec Creation Loading Badge */}
|
||||
{isCreatingSpec &&
|
||||
currentProject?.path === creatingSpecProjectPath && (
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-brand-500/10 border border-brand-500/20 shrink-0"
|
||||
title="Creating App Specification"
|
||||
data-testid="spec-creation-badge"
|
||||
>
|
||||
<Loader2 className="w-3 h-3 animate-spin text-brand-500 shrink-0" />
|
||||
<span className="text-xs font-medium text-brand-500 whitespace-nowrap">
|
||||
Creating spec
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Board Background & Detail Level Controls */}
|
||||
|
||||
@@ -248,6 +248,7 @@ export function InterviewView() {
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, "");
|
||||
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
return `<project_specification>
|
||||
<project_name>${projectName || "my-project"}</project_name>
|
||||
|
||||
|
||||
@@ -279,7 +279,18 @@ export function SpecView() {
|
||||
|
||||
const unsubscribe = api.specRegeneration.onEvent(
|
||||
(event: SpecRegenerationEvent) => {
|
||||
console.log("[SpecView] Regeneration event:", event.type);
|
||||
console.log(
|
||||
"[SpecView] Regeneration event:",
|
||||
event.type,
|
||||
"for project:",
|
||||
event.projectPath
|
||||
);
|
||||
|
||||
// Only handle events for the current project
|
||||
if (event.projectPath !== currentProject?.path) {
|
||||
console.log("[SpecView] Ignoring event - not for current project");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "spec_regeneration_progress") {
|
||||
// Extract phase from content if present
|
||||
|
||||
@@ -255,6 +255,7 @@ export function WelcomeView() {
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with the project name
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
@@ -352,6 +353,7 @@ export function WelcomeView() {
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with template-specific info
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
@@ -456,6 +458,7 @@ export function WelcomeView() {
|
||||
}
|
||||
|
||||
// Update the app_spec.txt with basic info
|
||||
// Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts
|
||||
await api.writeFile(
|
||||
`${projectPath}/.automaker/app_spec.txt`,
|
||||
`<project_specification>
|
||||
|
||||
Reference in New Issue
Block a user